Delete nested key from dictionary

Say I have a pretty complicated 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 that can be added to something that I can work with while reading.

However, I am currently writing unit tests where I need to selectively break dictionaries in several ways.

But I do not know how to gracefully 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 is 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 } 

Should there really be a more elegant way?

Ideally, I would write a test helper that takes a KVC string and has a signature like:

 func dictWithoutKeyPath(_ path: String) -> [String: Any] 

In the case of "lat" I would call it dictWithoutKeyPath("countries.japan.capital.lat") .

+8
collections dictionary ios swift nsdictionary
source share
5 answers

When working with an index, if the index is get / set and the variable is volatile, then the whole expression is mutable. However, due to the type of casting, the expression "loses" variability. (This is not l-value ).

The shortest way to resolve this issue is to create an index 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 do a (public) Swift Talk episode about it: mutating untyped dictionaries

+5
source share

You can create recursive methods (read / write) that visit your given key path by repeating attempts to convert (sub) dictionary values ​​into [Key: Any] dictionaries themselves. Also, allow public access to these methods using the new subscript .

Note that you may need to explicitly import Foundation to access the components(separatedBy:) String (bridge) method.

 extension Dictionary { subscript(keyPath keyPath: String) -> Any? { get { guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath) else { return nil } return getValue(forKeyPath: keyPath) } set { guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath), let newValue = newValue else { return } self.setValue(newValue, forKeyPath: keyPath) } } static private func keyPathKeys(forKeyPath: String) -> [Key]? { let keys = forKeyPath.components(separatedBy: ".") .reversed().flatMap({ $0 as? Key }) return keys.isEmpty ? nil : keys } // recursively (attempt to) access queried subdictionaries // (keyPath will never be empty here; the explicit unwrapping is safe) private func getValue(forKeyPath keyPath: [Key]) -> Any? { guard let value = self[keyPath.last!] else { return nil } return keyPath.count == 1 ? value : (value as? [Key: Any]) .flatMap { $0.getValue(forKeyPath: Array(keyPath.dropLast())) } } // recursively (attempt to) access the queried subdictionaries to // finally replace the "inner value", given that the key path is valid private mutating func setValue(_ value: Any, forKeyPath keyPath: [Key]) { guard self[keyPath.last!] != nil else { return } if keyPath.count == 1 { (value as? Value).map { self[keyPath.last!] = $0 } } else if var subDict = self[keyPath.last!] as? [Key: Value] { subDict.setValue(value, forKeyPath: Array(keyPath.dropLast())) (subDict as? Value).map { self[keyPath.last!] = $0 } } } } 

Setup Example

 // your example dictionary var dict: [String: Any] = [ "countries": [ "japan": [ "capital": [ "name": "tokyo", "lat": "35.6895", "lon": "139.6917" ], "language": "japanese" ] ], "airports": [ "germany": ["FRA", "MUC", "HAM", "TXL"] ] ] 

Usage example:

 // read value for a given key path let isNil: Any = "nil" print(dict[keyPath: "countries.japan.capital.name"] ?? isNil) // tokyo print(dict[keyPath: "airports"] ?? isNil) // ["germany": ["FRA", "MUC", "HAM", "TXL"]] print(dict[keyPath: "this.is.not.a.valid.key.path"] ?? isNil) // nil // write value for a given key path dict[keyPath: "countries.japan.language"] = "nihongo" print(dict[keyPath: "countries.japan.language"] ?? isNil) // nihongo dict[keyPath: "airports.germany"] = (dict[keyPath: "airports.germany"] as? [Any] ?? []) + ["FOO"] dict[keyPath: "this.is.not.a.valid.key.path"] = "notAdded" print(dict) /* [ "countries": [ "japan": [ "capital": [ "name": "tokyo", "lon": "139.6917", "lat": "35.6895" ], "language": "nihongo" ] ], "airports": [ "germany": ["FRA", "MUC", "HAM", "TXL", "FOO"] ] ] */ 

Note that if the supplied key path does not exist for the destination (using setter), this will not lead to the creation of an equivalent nested dictionary, but simply will not lead to a mutation of the dictionary.

+1
source share

Interest Ask. The problem is that the Swift fast binding mechanism, which is usually capable of mutating nested dictionaries, disables the necessary types from Any to [String:Any] . Therefore, when access to a nested element only becomes unreadable (due to types):

 // Eg Accessing countries.japan.capital ((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] 

... the mutation of the nested element does not even work:

 // Want to mutate countries.japan.capital.name. // The typecasts destroy the mutating optional chaining. ((((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] as? [String:Any])?["name"] as? String) = "Edo" // Error: Cannot assign to immutable expression 

Possible Solution

The idea is to get rid of the untyped dictionary and convert it into a strongly typed structure, where each element has the same type. I admit this is a tough decision, but in the end it works great.

An enumeration with related values ​​will work well for our custom type, which replaces an untyped dictionary:

 enum KeyValueStore { case dict([String: KeyValueStore]) case array([KeyValueStore]) case string(String) // Add more cases for Double, Int, etc. } 

An enumeration has one case for each type of expected element. Three cases cover your example, but you can easily expand it to cover more types.

Next, we define two indexes: one for the dictionary access key (with strings) and one for indexed access to the array (with integers). The subscripts check to see if self .dict or .array respectively, and if so, return the value to the given key / index. They return nil if the type does not match, for example. if you tried to access the .string value. Substrings also have setters. This is the key to working with a mutation in a chain:

 extension KeyValueStore { subscript(_ key: String) -> KeyValueStore? { // If self is a .dict, return the value at key, otherwise return nil. get { switch self { case .dict(let d): return d[key] default: return nil } } // If self is a .dict, mutate the value at key, otherwise ignore. set { switch self { case .dict(var d): d[key] = newValue self = .dict(d) default: break } } } subscript(_ index: Int) -> KeyValueStore? { // If self is an array, return the element at index, otherwise return nil. get { switch self { case .array(let a): return a[index] default: return nil } } // If self is an array, mutate the element at index, otherwise return nil. set { switch self { case .array(var a): if let v = newValue { a[index] = v } else { a.remove(at: index) } self = .array(a) default: break } } } } 

Finally, we add some convenience initializers to initialize our type with a dictionary, array, or string literals. They are not strictly necessary, but make working with a type easier:

 extension KeyValueStore: ExpressibleByDictionaryLiteral { init(dictionaryLiteral elements: (String, KeyValueStore)...) { var dict: [String: KeyValueStore] = [:] for (key, value) in elements { dict[key] = value } self = .dict(dict) } } extension KeyValueStore: ExpressibleByArrayLiteral { init(arrayLiteral elements: KeyValueStore...) { self = .array(elements) } } extension KeyValueStore: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .string(value) } init(extendedGraphemeClusterLiteral value: String) { self = .string(value) } init(unicodeScalarLiteral value: String) { self = .string(value) } } 

And here is an example:

 var keyValueStore: KeyValueStore = [ "countries": [ "japan": [ "capital": [ "name": "tokyo", "lat": "35.6895", "lon": "139.6917" ], "language": "japanese" ] ], "airports": [ "germany": ["FRA", "MUC", "HAM", "TXL"] ] ] // Now optional chaining works: keyValueStore["countries"]?["japan"]?["capital"]?["name"] // .some(.string("tokyo")) keyValueStore["countries"]?["japan"]?["capital"]?["name"] = "Edo" keyValueStore["countries"]?["japan"]?["capital"]?["name"] // .some(.string("Edo")) keyValueStore["airports"]?["germany"]?[1] // .some(.string("MUC")) keyValueStore["airports"]?["germany"]?[1] = "BER" keyValueStore["airports"]?["germany"]?[1] // .some(.string("BER")) // Remove value from array by assigning nil. I'm not sure if this makes sense. keyValueStore["airports"]?["germany"]?[1] = nil keyValueStore["airports"]?["germany"] // .some(array([.string("FRA"), .string("HAM"), .string("TXL")])) 
+1
source share

I would like to continue my previous answer with a different solution. This extends the Swift Dictionary type with a new index that takes a key path.

First, I introduce a new type called KeyPath to represent the key path. This is not strictly necessary, but facilitates working with key paths, as it allows us to wrap the logic of dividing the key path into its components.

 import Foundation /// Represents a key path. /// Can be initialized with a string of the form "this.is.a.keypath" /// /// We can't use Swift #keyPath syntax because it checks at compilet time /// if the key path exists. struct KeyPath { var elements: [String] var isEmpty: Bool { return elements.isEmpty } var count: Int { return elements.count } var path: String { return elements.joined(separator: ".") } func headAndTail() -> (String, KeyPath)? { guard !isEmpty else { return nil } var tail = elements let head = tail.removeFirst() return (head, KeyPath(elements: tail)) } } extension KeyPath { init(_ string: String) { elements = string.components(separatedBy: ".") } } extension KeyPath: ExpressibleByStringLiteral { init(stringLiteral value: String) { self.init(value) } init(unicodeScalarLiteral value: String) { self.init(value) } init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } 

Next, I create a dummy protocol called StringProtocol , which we need to limit to our Dictionary extension later. Swift 3.0 does not yet support extensions for generic types that restrict the general parameter to a specific type (for example, extension Dictionary where Key == String ). Support for this is planned for Swift 4.0, but before that we need this small workaround:

 // We need this because Swift 3.0 doesn't support extension Dictionary where Key == String protocol StringProtocol { init(string s: String) } extension String: StringProtocol { init(string s: String) { self = s } } 

Now we can write new indexes. The implementation for getter and setter is quite long, but they should be simple: we go from beginning to end, and then we get / set the value at this position:

 // We want extension Dictionary where Key == String, but that not supported yet, // so work around it with Key: StringProtocol. extension Dictionary where Key: StringProtocol { subscript(keyPath keyPath: KeyPath) -> Any? { get { guard let (head, remainingKeyPath) = keyPath.headAndTail() else { return nil } let key = Key(string: head) let value = self[key] switch remainingKeyPath.isEmpty { case true: // Reached the end of the key path return value case false: // Key path has a tail we need to traverse switch value { case let nestedDict as [Key: Any]: // Next nest level is a dictionary return nestedDict[keyPath: remainingKeyPath] default: // Next nest level isn't a dictionary: invalid key path, abort return nil } } } set { guard let (head, remainingKeyPath) = keyPath.headAndTail() else { return } let key = Key(string: head) // Assign new value if we reached the end of the key path guard !remainingKeyPath.isEmpty else { self[key] = newValue as? Value return } let value = self[key] switch value { case var nestedDict as [Key: Any]: // Key path has a tail we need to traverse nestedDict[keyPath: remainingKeyPath] = newValue self[key] = nestedDict as? Value default: // Invalid keyPath return } } } } 

And here is what it looks like to use:

 var dict: [String: Any] = [ "countries": [ "japan": [ "capital": [ "name": "tokyo", "lat": "35.6895", "lon": "139.6917" ], "language": "japanese" ] ], "airports": [ "germany": ["FRA", "MUC", "HAM", "TXL"] ] ] dict[keyPath: "countries.japan"] // ["language": "japanese", "capital": ["lat": "35.6895", "name": "tokyo", "lon": "139.6917"]] dict[keyPath: "countries.someothercountry"] // nil dict[keyPath: "countries.japan.capital"] // ["lat": "35.6895", "name": "tokyo", "lon": "139.6917"] dict[keyPath: "countries.japan.capital.name"] // "tokyo" dict[keyPath: "countries.japan.capital.name"] = "Edo" dict[keyPath: "countries.japan.capital.name"] // "Edo" dict[keyPath: "countries.japan.capital"] // ["lat": "35.6895", "name": "Edo", "lon": "139.6917"] 

I really like this solution. This is quite a bit of code, but you only need to write it once, and I think it looks very nice to use.

+1
source share

Pass the dictionary to this function, it will return you a flat dictionary without an embedded file attachment.

// SWIFT 3.0

 func concatDict( dict: [String: Any])-> [String: Any]{ var dict = dict for (parentKey, parentValue) in dict{ if let insideDict = parentValue as? [String: Any]{ let keys = insideDict.keys.map{ return parentKey + $0 } for (key, value) in zip(keys, insideDict.values) { dict[key] = value } dict[parentKey] = nil dict = concatDict(dict: dict) } } return dict } 
0
source share

All Articles