This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 10% discount for team members. Become a Subscriber

Mutating a nested untyped dictionary can be a challenge. To solve it we discuss the mutability of value types and the concept of l-values.

0:06 Today, let's have a look at mutating untyped dictionaries. This is a question we found on StackOverflow, and the solution turned out to be quite complex, but very interesting. You wouldn't usually solve the problem this way, but there are some interesting lessons: how Swift works with mutability, value types, and so on.

0:37 The problem is that we have an untyped dictionary, and we want to mutate something deep inside of its structure. For example, we want to change the name of the capital in the following dictionary:

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

1:04 Usually, when we have a dictionary like this, we'd parse it into a number of structs, then change the structs, and serialize it back to a dictionary. However, today, we want to directly mutate the dictionary, rather than going through a conversion process. It's an interesting exercise.

Accessing the Values

1:38 Just accessing something in the dictionary is already complicated. In a language like Javascript, you would write something like dict["countries"]["japan"], but that doesn't work in Swift. First of all, the result of dict["countries"] is optional, so we need to use optional chaining. Second, the result is of type Any?, so we can't write the second subscript.

2:16 In order to use a subscript, we have to add an optional cast to [String:Any] so that we can use the next subscript:

(dict["countries"] as? [String:Any])?["japan"]

Before we continue, we'll simplify the dictionary just a little bit:

var dict: [String:Any] = [
    "countries": [
        "japan": [
            "capital": "tokyo"
        ]
    ]
]

2:56 To look up the capital, we have to repeat the process: cast the entire expression, and use optional chaining. Finally, we can access the value:

((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"]

Assigning New Values

3:32 What we really want to do though is to assign a new value like this:

((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] = "berlin"
// doesn't compile

However, the code above doesn't work. Before we go into the reason why it breaks, we'll first make it work in a different way.

3:57 The simplest way to make it work is typing out all the nesting levels manually:

if var countries = dict["countries"] as? [String:Any],
   var japan = countries["japan"] as? [String:Any] {
    japan["capital"] = "berlin"
    countries["japan"] = japan
    dict["countries"] = countries
}

What About NSDictionary?

5:11 First, we thought mutating an untyped dictionary like this would be a lot easier using NSDictionary. We can use key-value coding, and all of the code would be simpler, so let's try that.

5:43 To access the value, we can simply use value(forKeyPath:):

import Foundation
(dict as NSDictionary).value(forKeyPath: "countries.japan.capital")

6:15 The code above is short and readable, and because we're dealing with untyped dictionaries anyway, it's just as safe as what we had before. However, it turns out we can't use KVC in the same way to change the value. The following code crashes:

(dict as NSDictionary).setValue("berlin", forKeyPath: "countries.japan.capital")
// crashes

First of all, the dictionary should be mutable:

(NSMutableDictionary(dictionary: dict)).setValue("berlin", forKeyPath: "countries.japan.capital")
// still crashes

7:14 However, this also crashes. Although the outer dictionary is now mutable, all the nested dictionaries are still immutable. That's why we crash with an error saying the instance isn't key-value coding compliant. NSDictionary doesn't really help us here compared to Swift's Dictionary in making changes to nested untyped dictionaries.

Casting and l-values

7:45 In order to find a solution, we first need to understand why the single-line solution we wrote out previously doesn't work:

((dict["countries"] as? [String:Any])?["japan"] as? [String:Any])?["capital"] = "berlin"
// doesn't compile

To explain what's going on, we'll create a more simple example. When we have a var variable of type Any, we can assign a new value:

var x: Any = 1
x = 2

8:25 As soon as we cast the variable to a different type (with the cast succeeding), we cannot mutate it anymore:

var x: Any = 1
(x as? Int) = 2 // doesn't compile

8:40 This is related to a concept called l-values. l-values are expressions that are allowed to be on the left-hand side of an assignment operator. We all know the let and var keywords, which control mutability. If you declare something with let, we can't use it as an l-value. It turns out that there are other things that influence the "l-valueness" of a variable. For example, a cast removes the "l-valueness" of an expression, even if it's a var, like in the example above.

9:24 Next to variables decalred with var, there are some other things that are l-values. For example, computed properties that have a getter and a setter are l-values. Likewise, subscripts can be used as an l-value if they have a setter. Finally, optional chaining propagates "l-valueness": if the expression was an l-value before, then after adding an optional chaining operator it's still an l-value. We can use that knowledge to come up with a simpler solution.

Using Custom Subscripts

10:20 We use a subscript to combine the casting with providing the setter. First, we add the getter for the subscript in an extension to Dictionary. The result of the subscript is [String:Any]?:

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

11:14 This already helps with the readability of accessing our dictionary:

dict[jsonDict: "countries"]?[jsonDict: "japan"]?["capital"]

11:47 It's not as clean as the key-value coding version, but at least it's still simple.

11:54 We still can't mutate, and for this we have to add a setter to the subscript so that we can use the subscript as an l-value. Within the setter, we simply assign the newValue and cast it to the Value type:

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

Now we can mutate a value within a nested dictionary like this:

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

12:47 This solution is straightforward, but we can only assign new values, not mutate existing values. Let's say we want to append to the existing string. The problem with our current expression is that its type is Any?. In order to mutate, we need a String?. Of course, once we add a type-cast, it's not an l-value anymore.

13:30 Luckily, we can copy-paste our existing subscript and modify it for Strings:

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

13:54 Now we can mutate a string value within the untyped dictionary like this:

dict[jsonDict: "countries"]?[jsonDict: "japan"]?[string: "capital"]?.append("!")

14:17 Even though we usually woudn't work on an untyped (e.g. JSON) dictionary like this, it's still an interesting problem, because we were forced to think about l-values, and when something is mutable. It's also interesting to have these custom subscripts. In some cases they can really help to make our code more readable. These are some good tools to deal with all the untypedness of JSON.