A safer approach to JSON parsing in Swift

If you’re using Swift 4, read this article instead. The article below has been fully updated for Swift 1.2, but everything is different in Swift 4. But you already knew that.

If you made it to the Beginning Swift workshop, you saw that we used valueForKeyPath: to parse the JSON from iTunes like this:

var jsonError: NSError?
let json = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &jsonError) as NSDictionary

if let unwrappedError = jsonError {
    println("json error: \(unwrappedError)")
} else {
    self.titles = json.valueForKeyPath("feed.entry.im:name.label") as [String]
}

And during the workshop, I mentioned that there are better ways to parse JSON. But what’s wrong with the above approach? Well, it works just fine when we get the response we expect. But what if we get a response we don’t expect?

The code above is missing all the type checks, which is what Swift does so well for us. It doesn’t handle unexpected responses, since there’s no checking in valueForKeyPath – it just tries to grab the value, and if it fails, it crashes. If, for example, the feed doesn’t contain the entry we expect, our app will crash. Below is a much safer way to parse JSON using optional binding:

func titlesFromJSON(data: NSData) -> [String] {
    var titles = [String]()
    var jsonError: NSError?
    
    if let json = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &jsonError) as? [String: AnyObject],
        feed = json["feed"] as? [String: AnyObject],
        entries = feed["entry"] as? [[String: AnyObject]]
    {
        for entry in entries {
            if let name = entry["im:name"] as? [String: AnyObject],
                label = name["label"] as? String {
                    titles.append(label)
            }
        }
    } else {
        if let jsonError = jsonError {
            println("json error: \(jsonError)")
        }
    }
    
    return titles
}

So this time, when we parse our JSON, we’re using the if let syntax to bind new constants to our optionals. And we’re checking the types on each to ensure that we have the data we expected. Let’s start with the first if let statement:

if let json = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &jsonError) as? [String: AnyObject],
        feed = json["feed"] as? [String: AnyObject],
        entries = feed["entry"] as? [[String: AnyObject]]
    {
    // this code is executed if the json is a dictionary AND
    // the "feed" is a dictionary AND
    // the "entry" is an array of dictionaries
} else {
    // otherwise, this code is executed
}

So in the first line, we’re turning our JSON into an object and checking to see whether it’s a dictionary with String keys and AnyObject values. If it’s a dictionary, the next line is executed, searching for a “feed” key. If there is one, and it’s a dictionary, the next line looks for an “entry” key. And if there’s an “entry” and it can be cast to an array of dictionaries, the if block is executed, and we can then use the entries array inside the if block. Otherwise, the else block is executed.

This optional binding continues through the code, though I’ve left out the else blocks for the other if let statements for brevity. You could certainly include else blocks after each if let if you wanted to do more extensive error handling.

The source code for this project is on GitHub – you can check out the JSONParser class, and see both the titlesFromJSON (the better, safer approach) and compare that to the bad-json branch – which uses the more dangerous approach.

Happy JSON parsing!