How to Deal With JSON in Swift

Posted by Grego on January 11, 2017

I spent hours yesterday fiddling with Swift’s native NSJSONSerialization class and Dictionaries and I finally figured out how to deal with JSON in Swift. The answer? One word.

DON’T

Seriously, use SwiftyJSON.

Were my large letters not convincing enough? Read on for a tale of peril and defeat…

So, in reality, at first it doesn’t seem so bad. Let’s look at a simple example:

JSON:

{
  "name": "George",
  "age": 21
}

Swift:

let jsonString = "{ \"name\": \"George\", \"age\": 21 }"
if let jsonData = jsonString.data(using: .utf8) {
  do {
    let jsonDictionary = try JSONSerialization.jsonObject(with: jsonData,
        options: [])
    print("name: \(jsonDictionary["name"])")
  }
  catch {
    print("Could not deserialize JSON: \(error.localizedDescription)")
  }
}

Looks like it should work, right? Those with enough experience with JSON deserialization and Swift dictionaries might spot the error. Even then it’s easy to overlook. If you try to compile the code above you’ll end up with the following error:

Type ‘Any’ has no subscript members

This is what happens when we try to pull out jsonDictionary["name"].

Oh that’s right! We forgot to cast to a dictionary. So silly. Let’s just fix that real quick and be on our way:

let jsonString = "{ \"name\": \"George\", \"age\": 21 }"
if let jsonData = jsonString.data(using: .utf8) {
  do {
    if let jsonDictionary = try JSONSerialization.jsonObject(with: jsonData,
        options: []) as? [String:Any] {
      let name = jsonDictionary["name"]
      let age = jsonDictionary["age"]
      print("\(name) is \(age) years old.")
    }
  }
  catch {
    print("Could not deserialize JSON: \(error.localizedDescription)")
  }
}

I also pulled out name and age to separate variables because darn is it tough to type those dictionary keys in interpolated string notation. If you compile the above snippet, you’ll find it works and you get the following output:

Optional(George) is Optional(21) years old.

Well, gee, that’s not what we wanted. Let’s just unwrap those optionals:

let jsonString = "{ \"name\": \"George\", \"age\": 21 }"
if let jsonData = jsonString.data(using: .utf8) {
  do {
    if let jsonDictionary = try JSONSerialization.jsonObject(with: jsonData,
        options: []) as? [String:Any] {
      if let name = jsonDictionary["name"],
          let age = jsonDictionary["age"] {
        print("\(name) is \(age) years old.")
      }
    }
  }
  catch {
    print("Could not deserialize JSON: \(error.localizedDescription)")
  }
}

Now we get the output we were looking for:

George is 21 years old.

However, this only works if we have both a name and an age, we haven’t handled other possible scenarios yet. If we wanted to check individually we’d have to break that down into two separate if-lets. You can see where this is going…

Now let’s try a more involved example with sub-objects, something you’re more likely to see in the real world:

Given this JSON:

{
  "class": "Computer Science",
  "instructor": {
    "name": "Henry",
    "age": 60
  },
  "students": [
    {
      "name": "Andre",
      "age": 25
    },
    {
      "name": "Michael",
      "age": 21
    },
    {
      "name": "George",
      "age": 21
    }
  ]
}

As far as JSON goes, this is relatively simple. Let’s try to pull out some information now:

let classString = "{ \"class\": \"Computer Science\", \"instructor\": { \"name\": \"Henry\", \"age\": 60 }, \"students\": [ { \"name\": \"Andre\", \"age\": 25 }, { \"name\": \"Michael\", \"age\": 21 }, { \"name\": \"George\", \"age\": 21 } ] } "
if let jsonData = classString.data(using: .utf8) {
  do {
    if let jsonDictionary = try JSONSerialization.jsonObject(with: jsonData,
        options: []) as? [String:Any] {
      if let className = jsonDictionary["class"] as? String,
          let instructor = jsonDictionary["instructor"] as? [String:Any],
          let instructorName = instructor["name"] as? String,
          let instructorAge = instructor["age"] as? Int,
          let students = jsonDictionary["students"] as? [[String:Any]] {
        print("The class is \(className). The instructor is \(instructorName). "
            + "He is \(instructorAge) years old. "
            + "The class has \(students.count) students:")
        for student in students {
          if let studentName = student["name"] as? String,
              let studentAge = student["age"] as? Int {
            print("\(studentName), \(studentAge) years old")
          } // what
        } // the
      }
    } // just
  } // happened?!
  catch {
    print("Could not deserialize JSON: \(error.localizedDescription)")
  }
}

This is the (mostly final) sample that prints out:

The class is Computer Science. The instructor is Henry. He is 60 years old. The class has 3 students:
Andre, 25 years old
Michael, 21 years old
George, 21 years old

Lovely. Look at that indentation! And we only went two levels deep in the JSON!

Eddie: Well sure it was a war. And anybody that showed up was gonna join Lem Lee in the Hell of Being Cut to Pieces.

Jack Burton: Hell of being what?

Eddie: Chinese have a lot of Hells.

You are now entering the hell of unwrapping.

Jack Burton eating noodles

Fast forward a bit and kick it up a notch with SwiftyJSON:

let classString = "{ \"class\": \"Computer Science\", \"instructor\": { \"name\": \"Henry\", \"age\": 60 }, \"students\": [ { \"name\": \"Andre\", \"age\": 25 }, { \"name\": \"Michael\", \"age\": 21 }, { \"name\": \"George\", \"age\": 21 } ] } "

let classJson = JSON(data: classString.data(using: .utf8)!)

if let className = classJson["class"].string,
  let instructorName = classJson["instructor"]["name"].string,
  let instructorAge = classJson["instructor"]["age"].int,
  let students = classJson["students"].array {
  print("The class is \(className). The instructor is \(instructorName). "
      + "He is \(instructorAge) years old."
      + "The class has \(students.count) students: ")

  for student in students {
    if let studentName = student["name"].string,
        let studentAge = student["age"].int {
      print("\(studentName), \(studentAge) years old")
    }
  }
}

The above produces the same output:

The class is Computer Science. The instructor is Henry. He is 60 years old.The class has 3 students: 
Andre, 25 years old
Michael, 21 years old
George, 21 years old

Note I also force unwrapped the Data conversion from String, which could potentially fail. You can use the same if let construct from previous examples, which would indent us one more level.

I could have also forced unwrapped those values instead of using if let to simplify it even further. In which case, sensible defaults are used. Here is an example for illustration:

let className = classJson["class"].stringValue
let instructorName = classJson["instructor"]["name"].stringValue
let instructorAge = classJson["instructor"]["age"].intValue
let students = classJson["students"].arrayValue

print("The class is \(className). The instructor is \(instructorName). "
  + "He is \(instructorAge) years old."
  + "The class has \(students.count) students: ")

for student in students {
  let studentName = student["name"].stringValue
  let studentAge = student["age"].intValue

  print("\(studentName), \(studentAge) years old")
}

The xxxValue properties will return a value that is guaranteed to be that type. The decision to force the value with the xxxValue method or to gracefully unwrap using the regular xxx properties is up to you. In practice, it’s better to mix and match depending on your use case.

Read more on the SwiftyJSON GitHub page