Remove nested key from dictionary

collections, dictionary, ios, nsdictionary, swift

Solution

When working with a subscript, if the subscript is get/set and the variable is mutable, then the entire expression is mutable. However, due to the type cast the expression "loses" the mutability. (It's not an l-value anymore).

The shortest way to solve this is by creating a subscript that is get/set and does the conversion for you.

extension Dictionary {
    subscript(jsonDict key: Key) -> [String:Any]? {
        get {
            return self[key] as? [String:Any]
        }
        set {
            self[key] = newValue as? Value
        }
    }
}

Now you can write the following:

dict[jsonDict: "countries"]?[jsonDict: "japan"]?[jsonDict: "capital"]?["name"] = "berlin"

We liked this question so much that we decided to make a (public) Swift Talk episode about it: mutating untyped dictionaries

Problem

Let's say I have a rather complex dictionary, like this one: ``` let dict: [String: Any] = [ "countries": [ "japan": [ "capital": [ "name": "tokyo", "lat": "35.6895", "lon": "139.6917" ], "language": "japanese" ] ], "airports": [ "germany": ["FRA", "MUC", "HAM", "TXL"] ] ] ``` I can access all fields with `if let ..` blocks optionally casting to something that I can work with, when reading. However, I am currently writing unit tests where I need to selectively break dictionaries in multiple ways. But I don't know how to elegantly remove keys from the dictionary. For example I want to remove the key `"japan"` in one test, in the next `"lat"` should be nil. Here's my current implementation for removing `"lat"`: ``` if var countries = dict["countries"] as? [String: Any], var japan = countries["japan"] as? [String: Any], var capital = japan["capital"] as? [String: Any] { capital.removeValue(forKey: "lat") japan["capital"] = capital countries["japan"] = japan dictWithoutLat["countries"] = countries } ``` Surely there must be a more elegant way? Ideally I'd write a test helper that takes a KVC string and has a signature like: ``` func dictWithoutKeyPath(_ path: String) -> [String: Any] ``` In the `"lat"` case I'd call it with `dictWithoutKeyPath("countries.japan.capital.lat")`.

Original source