iOS App Development

Simple JSON Parsing with Swift

By Mike Woods, Atimi Software Inc.


Many mobile applications rely on JSON-based web services for their data. For iOS, there has long been ample JSON support through third-party libraries and, since iOS 5, NSJSONSerialization. The typical output of such is a dictionary containing other dictionaries, arrays, numbers and strings, which works very nicely for Objective-C but can present some problems for the strongly typed Swift. However, using Swift’s powerful support for enumerations, this common task can be just as straightforward without compromising good type checking.


The Joy of JSON


JSON is a lightweight data-interchange format that is great for many classes of web service. It allows the transmission of simple object graphs in an easy to read form (both for developers and code).

Here is an example of a contact record for Johnny Appleseed:
    

    {
	
    "type" : "person",
    "first name" : "Johnny",
    "last name" : "Appleseed",
    "nickname" : null,
    "age" : 42,

    "phone numbers" : [
        {
            "type" : “work",
            "number" : "+1 (555) 555-5555"
        },
        {
            "type" : "home",
            "number" : null
        }
    ],
    "work address" : {
        "line 1" : "1234 5th Avenue",
        "line 2" : null,
	"city" : "Hometown"
    },
    "home address" : null
    
}

Whether or not you have seen JSON before, you have probably already figured out the data structure: the name and age are simple attributes; the work and home addresses are objects (though we don’t have his home address); and there is a list of phone numbers, each of which is its own object.


The Previous State of Art

Now, suppose you are writing an application that consumes a web service that returns the JSON example from above. Once the data has been read from the network it can be converted into a dictionary like so:

 

    

    NSError* error = nil;
    NSDictionary* jsonObject = [NSJSONSerialization JSONObjectWithData:data 
                                                           options:0 
                                                             error:&error];


(Actually, JSONObjectWithData:options:error: returns an ID but the code has been kept simple for illustration.)
With the object parsed, it’s possible to start extracting information of interest:
    

    NSString* firstName = jsonObject[@"first name"];
    int age = [jsonObject[@"age"] intValue];
    NSString* workCity = jsonObject[@"work address"][@"city"];
    NSLog(@"%@ is %d years and works in %@", firstName, age, workCity);
    // Johnny is 42 years and works in Hometown


That looks pretty straightforward but there is a gotcha lurking in the code. Although getting the work city was fine, the same code for Johnny’s home town will throw an exception. To see why it is necessary to understand what actually happens when the expression jsonObject[@“home address”][@“city”] is evaluated. At runtime, this breaks down into the following:
    

    id temp1 = [jsonObject objectForKey:@“home address”];
    id temp2 = [temp1 objectForKey:@“city”];
    NSString* homeCity = temp2;


Looking back at the sample JSON, “home address” is null, so you might think that temp1 will be set to nil and, as every good Objective-C developer knows, invoking a method on nil returns nil, which means homeCity will be nil, right?

Wrong. The trouble is that NSDictionary does not allow nil as a value so the parser sets the value to the null singleton, [NSNull null]. This is not the same as nil; it is an object, and invoking objectForKey: triggers an “unrecognized selector” exception.

To handle the sample correctly, we need to explicitly test for the missing address:
    

    NSString* homeCity = nil;
    id homeAddress = jsonObject[@"home address"];
    if ([homeAddress isKindOfClass:[NSDictionary class]])
    {
         homeCity = homeAddress[@"city"];
    }


Unfortunately, this is the tip of quite a large iceberg: what if “city” is null, or “first name”? And what about “age”? Suddenly, three lines of code have become twenty! Well, to be fair, you could condense the checks to keep to three lines but it gets pretty ugly…
    
    NSString* firstName = ([jsonObject[@"first name"] isKindOfClass:[NSString class]] ? 
    jsonObject[@"first name"] : nil);
    int age = ([jsonObject[@"age"] isKindOfClass:[NSNumber class]] ? [jsonObject[@"age"] intValue] : 0);
    NSString* workCity = ([jsonObject[@"work address"] isKindOfClass:[NSDictionary class]] ? 
    ([jsonObject[@"work address"][@"city"] isKindOfClass:[NSString class]] ? 
    jsonObject[@"work address"][@"city"] : nil) : nil);

Enter Swift

How does this same code play out in Swift? First, here is the equivalent of the first Objective-C version.
    

    var error: NSError? = nil;
    var jsonObject: AnyObject? = NSJSONSerialization.JSONObjectWithData(data!, 
    options: NSJSONReadingOptions.allZeros, error: &error)

    let firstName = (jsonObject as NSDictionary)["first name"] as String
    let age = (jsonObject as NSDictionary)["age"] as Int
    let workCity = ((jsonObject as NSDictionary)["work address"] as NSDictionary) ["city"] as String


Already you can see the type safety pushing into the frame with the explicit casts to NSDictionary. And in addition to the NSNull issue already highlighted, the Swift code will also fail if any of the JSON elements are plain missing, which would have worked in the Objective-C code. To successfully get the home city requires the following:
    

    var homeCity: String?
    if let jsonDict = jsonObject as? NSDictionary {
         if let address = jsonDict["home address"] as? NSDictionary {
	     if let city = address["city"] as? String {
	         homeCity = city
	     }
         }
     }


As with Objective-C, the Swift code can be collapsed into a single line, as long as you’re prepared for a very long line full of parentheses and question marks.

Wouldn’t it be good if you could have the compact style of the original Objective-C with all the type safety of Swift. How about something like this?
    
   let jsonObject = JSON.parse(data!)
   let firstName = jsonObject?["first name"]?.string
   let age = jsonObject?["age"]?.int
   let workCity = jsonObject?["work address"]?["city"]?.string


A Node by Any Other Name

What we want to make this happen is some sort of wrapper around the data that can do all the heavy lifting for us without getting in the way of chaining down the data tree to get at deeply embedded values. It turns out that Swift enumerations are an ideal solution.

Firstly, we want to define an enumeration that can wrap a JSON value. Because Swift enumerations can have associated values, we can neatly package type-dependent values.
    
   public enum JSON : Printable {
        case Array([JSON])
        case Dictionary([Swift.String: JSON])
        case String(Swift.String)
        case Number(Float)
        case Null
   }


Though very neat, it’s not very useful in itself. However, because enumerations are first-class types, we can add code to do all that work we had to previously do by hand. (Note that all the methods that follow belong to the enum – we are just presenting them separately for annotation.)

The first task is to generate a wrapped JSON value. Let’s define a static method to do this:
    
   public static func wrap(json: AnyObject) -> JSON {
    if let str = json as? Swift.String {
	return .String(str)
    }
    if let num = json as? NSNumber {
	return .Number(num.floatValue)
    }
    if let dictionary = json as? [Swift.String: AnyObject] {
	return .Dictionary(dictionary)
    }
    if let array = json as? [AnyObject] {
	return .Array(array)
    }
    assert(json is NSNull, "Unsupported Type")
    return .Null
    }


The method simply checks the supplied value and returns the appropriate enum with the value attached.

While we’re thinking about wrapping, we might as well define a convenience method to parse JSON data into our enum.
    
   public static func parse(data: NSData) -> JSON? {
       var error: NSError?
       if let json: AnyObject = NSJSONSerialization.JSONObjectWithData(data as NSData, 
       options: NSJSONReadingOptions.AllowFragments, error: &error) {
	   return wrap(json)
       }
       return nil
    }


Pretty straightforward: use NSJSONSerialization to parse the data and, if successful, wrap it in our enum. Of course, for a full implementation you may want some error handling, and probably define other parse methods that take the data in other formats.

Having created a JSON enum, let’s define ways of getting at the data. First, we can define computed properties that provide type-safe access to simple values:
    
    public var string: Swift.String? {
          switch self {
              case .String(let s):
	          return s
	      default:
	          return nil
          }
      }

      public var int: Int? {
          switch self {
	      case .Number(let d):
	          return Int(d)
	      default:
	          return nil
          }
      }
	
      public var float: Float? {
          switch self {
	      case .Number(let d):
	          return d
	      default:
	          return nil
          }
       }
	
      public var bool: Bool? {
          switch self {
	      case .Number(let d):
	          return (d != 0)
	      default:
	           return nil
          }
      }


Notice that, in each case, the value is returned if it matches the expected type, otherwise the result is nil. Testing for null itself requires a slightly different pattern but the principle is the same.
    
   public var isNull: Bool {
      switch self {
	case Null:
	    return true
	default:
	    return false
      }
    }


For returning dictionaries and arrays, we need to do more work because we need to wrap each value in the collection with a JSON enumeration value.
    
   public var dictionary: [Swift.String: JSON]? {
      switch self {
	  case .Dictionary(let d):
	      var jsonObject: [Swift.String: JSON] = [:]
	      for (k,v) in d {
		  jsonObject[k] = JSON.wrap(v)
	      }
	      return jsonObject
	  default:
	      return nil
      }
  }
	
  public var array: [JSON]? {
      switch self {
	  case .Array(let array):
	      let jsonArray = array.map({ JSON.wrap($0) })
	      return jsonArray
	  default:
	      return nil
      }
  }


So now it’s possible to do something like
    
   let age2 = jsonObject?.dictionary?[“age"]?.int?


but that’s pretty ugly (not to mention inefficient) so we can go further and eliminate the explicit test for dictionary by defining the subscript methods.
    
    public subscript(index: Swift.String) -> JSON? {
        switch self {
	    case .Dictionary(let dictionary):
	        if let value: AnyObject = dictionary[index] {
		    return JSON.wrap(value)
	        }
	        fallthrough
	    default:
	        return nil
        }
    }
	
    public subscript(index: Int) -> JSON? {
        switch self {
	    case .Array(let array):
	        return JSON.wrap(array[index])
	    default:
	        return nil
        }
    }


And that’s it.

The Acid Test

So, let’s go back and check that our example code does what we expect. Here is the code again from above.
    
     let jsonObject = JSON.parse(data!)
     let firstName = jsonObject?["first name"]?.string
     let age = jsonObject?["age"]?.int
     let workCity = jsonObject?["work address"]?["city"]?.string


Let’s first convert our JSON data (held in an NSData object) into a an optional JSON enumeration. To get the first name:

1. Unwrap the top-level object,
2. Look up the “first name” key, which returns any value as an optional JSON enumeration,
3. Unwrap it, and
4. Read it as an optional String value.

If any step isn’t as expected, the whole expression will simply result in nil.

It is the same pattern for Johnny’s age.

But what about the work city and home town that derailed our simplistic Objective-C in the first place? When we ask for “work address”, the code will return an JSON.Dictionary value and the city lookup will work as expected. In the case of “home address”, the code will return a JSON.Null so the “city” subscript will evaluate to nil (because the value is not JSON.Dictionary), and the optional unwrap will end the expression and return nil. No exception!


Wrapping it Up

Hopefully you will find this little enumeration useful in your own applications and inspire you to find many other great uses for Swift’s feature-rich enumerations. If you have any questions or want more information on Swift, feel free to give us a call. We’re always happy to chat.


778-372-2800


info@atimi.com