7

My question is simple, I want to know how to do a deep merge of 2 Swift dictionaries (not NSDictionary).

let dict1 = [
    "a": 1,
    "b": 2,
    "c": [
        "d": 3
    ],
    "f": 2
]

let dict2 = [
    "b": 4,
    "c": [
        "e": 5
    ],
    "f": ["g": 6]
]

let dict3 = dict1.merge(dict2)

/* Expected:

dict3 = [
    "a": 1,
    "b": 4,
    "c": [
        "d": 3,
        "e": 5
    ],
    "f": ["g": 6]
]

*/

When dict1 and dict2 have the same key, I expect the value to be replaced, but if that value is another dictionary, I expect it to be merged recursively.

Here is the solution I'd like:

protocol Mergeable {

    mutating func merge(obj: Self)

}

extension Dictionary: Mergeable {

    // if they have the same key, the new value is taken
    mutating func merge(dictionary: Dictionary) {
        for (key, value) in dictionary {
            let oldValue = self[key]

            if oldValue is Mergeable && value is Mergeable {
                var oldValue = oldValue as! Mergeable
                let newValue = value as! Mergeable

                oldValue.merge(newValue)

                self[key] = oldValue
            } else {
                self[key] = value
            }
        }
    }

}

but it gives me the error Protocol 'Mergeable' can only be used as a generic constraint because it has Self or associated type requirements

EDIT: My question is different from Swift: how to combine two Dictionary instances? because that one is not a deep merge.

With that solution, it would produce:

dict3 = [
    "a": 1,
    "b": 4,
    "c": [
        "e": 5
    ]
]
Community
  • 1
  • 1
Rodrigo Ruiz
  • 4,248
  • 6
  • 43
  • 75
  • 1
    Possible duplicate of [Swift: how to combine two Dictionary arrays?](http://stackoverflow.com/questions/26728477/swift-how-to-combine-two-dictionary-arrays) – Abhishek Dey Oct 04 '15 at 04:30
  • 1
    That one is not a deep merge. – Rodrigo Ruiz Oct 04 '15 at 04:32
  • In Swift, exactly what is the type of the dictionary you have in mind? – matt Oct 04 '15 at 04:45
  • It can be anything, that is the problem, it is not necessarily just 2 levels deep and only strings and numbers. – Rodrigo Ruiz Oct 04 '15 at 04:46
  • You can't have a Swift dictionary of anything. Answer the question, please. – matt Oct 04 '15 at 04:52
  • Also, tell me this: if "c" has a value that is a String in one dictionary and a dictionary in another, now what? I'm trying to show you that your spec is incoherent. – matt Oct 04 '15 at 04:53
  • let dict = [String: AnyObject] .... – Rodrigo Ruiz Oct 04 '15 at 04:55
  • Edited the question to add your case `"f": 2` and then `"f": [...]`. Basically, if both values are dictionaries, you do a deep merge, if one of the values is not a dictionary, you just replace the old with the new value. – Rodrigo Ruiz Oct 04 '15 at 04:58

3 Answers3

7

In my view the question is incoherent. This is an answer, however:

func deepMerge(d1:[String:AnyObject], _ d2:[String:AnyObject]) -> [String:AnyObject] {
    var result = [String:AnyObject]()
    for (k1,v1) in d1 {
        result[k1] = v1
    }
    for (k2,v2) in d2 {
        if v2 is [String:AnyObject], let v1 = result[k2] where v1 is [String:AnyObject] {
            result[k2] = deepMerge(v1 as! [String:AnyObject],v2 as! [String:AnyObject])
        } else {
            result[k2] = v2
        }
    }
    return result
}

Here is your test case:

    let dict1:[String:AnyObject] = [
        "a": 1,
        "b": 2,
        "c": [
            "d": 3
        ]
    ]

    let dict2:[String:AnyObject] = [
        "b": 4,
        "c": [
            "e": 5
        ]
    ]
    let result = deepMerge(dict1, dict2)
    NSLog("%@", result)
    /*
    {
        a = 1;
        b = 4;
        c =     {
            d = 3;
            e = 5;
        };
    }
    */

3rd-party edit: Alternate version using variable binding and newer Swift syntax.

func deepMerge(_ d1: [String: Any], _ d2: [String: Any]) -> [String: Any] {
    var result = d1
    for (k2, v2) in d2 {
        if let v1 = result[k2] as? [String: Any], let v2 = v2 as? [String: Any] {
            result[k2] = deepMerge(v1, v2)
        } else {
            result[k2] = v2
        }
    }
    return result
}
Quinn Taylor
  • 44,553
  • 16
  • 113
  • 131
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thank you, actually trying your solution (which I thought was the same as mine) made me realize that comparing a dictionary with [String: AnyObject?] wasn't working, I have to compare to [String: AnyObject]. There is only one more problem, how to accept [String: AnyObject?] – Rodrigo Ruiz Oct 04 '15 at 05:15
  • I already said the question is incoherent. I answered the question as far as you asked it. I'm not hanging around here while you keep shuffling the goalposts around. – matt Oct 04 '15 at 05:27
  • How is the question incoherent? I want a deep merge of a dictionary, like in any scripting language... – Rodrigo Ruiz Oct 04 '15 at 05:32
  • 1
    The question isn't incoherent at all, and this is a good solution. :-) – Quinn Taylor Oct 18 '19 at 02:44
1

How about manually doing it.

func += <KeyType, ValueType> (inout left: Dictionary<KeyType, ValueType>, right: Dictionary<KeyType, ValueType>) { 
    for (k, v) in right { 
        left.updateValue(v, forKey: k) 
    } 
}

You can also try ExSwift Library

Abhishek Dey
  • 1,601
  • 1
  • 15
  • 38
1
extension Dictionary {
    mutating func deepMerge(_ dict: Dictionary) {
        merge(dict) { (current, new) in
            if var currentDict = current as? Dictionary, let newDict = new as? Dictionary {
                currentDict.deepMerge(newDict)
                return currentDict as! Value
            }
            return new
        }
    }
}

How to use/test:

var dict1: [String: Any] = [
    "a": 1,
    "b": 2,
    "c": [
        "d": 3
    ],
    "f": 2
]

var dict2: [String: Any] = [
    "b": 4,
    "c": [
        "e": 5
    ],
    "f": ["g": 6]
]

dict1.deepMerge(dict2)

print(dict1)
River2202
  • 1,225
  • 13
  • 22
  • The example does not work correctly. I have tested it. The problem seems to be that the replace in merge does not work properly. Try and replace "f": 2 with "f": ["g": 2] (in dict1) and you will no receive a "f": ["g": 6] in the logged output but "f": ["g": 2] which is the old value (and incorrect). I wont downvote but please repair it. – Darkwonder Dec 03 '20 at 09:50
  • Yes, you are right, fixed and updated the answer. Please verify. Test code can be found here: https://github.com/river2202/MonorailSwift/blob/0f6aaf12706f2a4f47ec735c084a38998fad5cab/MonorailSwiftTests/MonorailSwiftTests.swift#L84 – River2202 Dec 04 '20 at 11:22