JSON parsing has been a persistent challenge for Swift developers. In the first six months of Swift’s release, perhaps more articles were published on the subject than any other, and JSON parsing libraries abound. In this talk, Anthony Levings looks at how we move from a loosely typed JSON environment to a strongly typed Swift environment. To make the most of type safety, we might want to consider alternative ways of phrasing the problem. Using two experimental parsers, Anthony does just that.
You can contribute to Anthony’s JSON parser, Aldwych, on GitHub.
JSON in Swift (0:00)
What I have for you today is not a solution that ends all other solutions to JSON parsing. I wrote some code while writing this talk, to work out how I would approach this problem. In fact, I ended up with two parsers, because I took a double-branch approach of trying to write a parser that was faster, and one that was better.
I want to walk through JSON, the reasons why it is a problem, and why incompatibilities happen. I made a prop to use in the beginning of my talk: a cardboard box, painted black, with a different class on each side. We start with the first side of the black box, this is an NSData
class. We go to NSData
to get what that we need, and it will send back an optional NSData?
containing our data. If we use if let
, our box turns into NSData
. This is a type that works with NSJSONSerialization
, and then turns into AnyObject?
.
Once we have done that, the resulting AnyObject?
is going to be either an array or a dictionary. After, we can cast that, and end up with an array that is half Swift and half not Swift, because inside is an array of AnyObject
s. Again, we have to go into the array and see exactly what type we have — is it a string? A number? We have an entire selection of types that AnyObject
might be. But look! We have another array or dictionary. The nesting can go on endlessly.
“Smash and Grab” (4:50)
I have an approach that puts safety last, which can be called a “Smash and Grab” approach when working with AnyObject
. We can just cast it to an int
, and should it be an int
, that will be what we get. This example involves accessing the iTunes API. I know that resultCount
is of type int
, so we get back a count that is casted to an int
. This works fairly well when you want to get values.
var error:NSError?
if let jsonObject = NSJSONSerialization.JSONObjectWithData(data,
options: nil, error: &error) as? [String: AnyObject] {
let count = jsonObject["resultCount"] as? Int
// casting value to Int
}
The other thing I want to talk about today is type safety. The problem with this AnyObject
is that I can change resultCount
to whatever I want. I can make that number a string. If there is not integrity to this data, I can change it back and nothing will stop me. The compiler won’t know that I’m making these changes.
var error:NSError?
if let jsonObject = NSJSONSerialization.JSONObjectWithData(data,
options: nil, error: &error) as? [String: AnyObject] {
var jDict = jsonObject
jDict["resultCount"] = "string"
// change of type can happen easily
let jsonData = NSJSONSerialization.dataWithJSONObject(jDict,
options: nil, error: nil)
}
Simple Safety: “Dream Data” (6:17)
Then I thought, “What are the other options?” One scenario happens if you get very, very lucky, and you have a dream set of data. This data is very simple, and looks like a dictionary or an array of ints. If that is what your JSON looks like, then you can simply cast a dictionary as <String,String>
, or the JSON as [Int]
. You are within the type safety zone of Swift. It now knows what type it will get when it accesses a value in your dictionary or array.
// if JSON looks like:
// {"key1":"value1","key2":"value2","key3":"value3"}
if let dict = jsonObject as? Dictionary<String,String> { }
// [1,2,3,4,5,6,6,7,8,9,10]
if let dict = jsonObject as? [Int] { }
But, this is not the real world, and JSON doesn’t usually look quite so nice. Particularly up to this point, there is often a lot of nesting and mixed types. That is a distinct incompatibility between the strongly typed Swift and the loose and free JSON. When we had Objective-C’s NSArray
and NSDictionary
, that wasn’t a problem. But, Swift demands something different.
Another option to make things very safe, would be to convert most things to a string. Thus, you could print something that had a mix of numbers and strings by converting everything to a string. However, you would then lose all sense of type when trying to convert back and forth.
Parser One: Safely Wrapped (8:09)
My next topic is enum
s with associated values. You can attach values to types in your enumeration, which enables you to know the type that you will get. When you work with JSON, there is no way to infer the type that you will get. Swift wants to know this all the time. This problem is always going to exist, where somewhere along the line, you have to know the type that you will get. The enum
approach enables us to wrap everything that we have.
enum Value {
// enum cases
case StringType(String)
case NumberType(NSNumber)
case NullType(NSNull)
// collection types
case DictionaryType(Dictionary<String,Value>)
case ArrayType([Value])
}
If we have a string, we can unwrap AnyObject
, and then wrap it up again as a string. We can do this for any of the cases in the enum. We do this because it helps us write cleaner code. We no longer have to parse around AnyObject
or have longer code trying to access it.
if let num = dictionary["key"]?.number { }
// rather than:
if let dict = jsonObj as? [String: AnyObject],
str = dict["key"] as? NSNumber { }
I have found three GitHub libraries that use this enum with associated values approach to wrapping JSON. First is Argo, which I will talk more about later. Swiftz is a very similar functional approach, but a larger library which JSON is a part of. json-swift is the third, and it follows that same approach. SwiftyJSON is another known parser for JSON in Swift, but instead it uses enums and stores AnyObject
. It doesn’t actually stores values in this wrapped enum type.
Type Safety = Empowerment (11:25)
You might wonder if you are truly making a change when you unwrap AnyObject
, only to wrap it up again, and you are! There are benefits to type safety. It restricts changes of type through subscripting, which I have found to be very valuable. If you are writing the subscripting yourself, you can specify the type that you want the JSON to remain as. Or, you can say that you don’t want anything to change at all. You can impose these restrictions yourself to keep type safety. Other benefits include preventing the return of AnyObject
. The compiler can better detect errors and thus assist the programmer. We can also reduce the amount of code.
The main thing that type safety makes us do is to think more about how we work with JSON. One of the problems I had when I first started working with JSON and Objective-C was time. For example, when you work with table views, you need to work very quickly. Tables views need to get data quickly and then refresh. You want JSON that is as simple as possible, so that working with it becomes very simple.
Potential Problems (14:06)
While looking on the Internet, I found it interesting that the creator of Argo, thoughtbot, also hit problems with nested JSON. I haven’t stress-tested Argo to know how bad the problem is, or where it happens, but this makes us really think about nesting. Some of the slow down is likely due to the fact that you have to wrap and unwrap everything — this can be really expensive!
This is the reason why I have written two parsers. The first one is very simple: it wraps everything using the approach that I talked about previously, with associated values in enums.
Parser Two: Wrapped on Demand (16:08)
THe second solution was to create a “wrap on demand” approach. This solution may be messier, but I don’t know if it is a long-term faster solution. I created two types, JSONDictionary
and JSONArray
. In each of these types are, essentially, five optional dictionaries. Every string in one level of your array is collected in one subsection of the box, while all the numbers are kept in another section. The dictionary is an unordered object in Swift. If you know where to look, it doesn’t matter if you split the types between separate objects.
enum Value {
// enum cases
case StringType(String)
case NumberType(NSNumber)
case NullType(NSNull)
// collection types
case DictionaryType(JSONDictionary)
case ArrayType(JSONArray)
}
In combining structs and enums, there is a certain leveraging. In the first stage of parsing, we have the basic parsing and setting code from the parser. You can get specific things out, which might take longer if we have wrapped everything, and must iterate through all the values to see what is inside each. Instead, with this struct approach, we can get to know whether the array has strings or even mixed types. We can get all the strings, remove all the strings, and so on.
// getting
if let p = parsedJSON["results"]?.jsonArr,
d = p[0]?.jsonDict {
d["trackName"]?.str
}
//setting
parsedJSON["results"]?[0]?["trackName"] = "Something"
Bespoke Handling of Data (18:16)
While I have been working on this idea of a parser, I have also been keeping on eye on how everyone else does it, as well as what people want from it. One of the things that the Argo readme starts with is a struct that knows what values it wants to fill out. If you give Argo a struct and parse it through its function, everything gets filled out.
My alternative approach was to build the parser first and use it with iTunes. It would know what the data from the iTunes API looks like, as well as its structure. Not only could the parser separate out data, but it could also put it back together and regenerate JSON. The tracks
type holds three values: the track ID, the track name, and the album name. If we update a track and send it back to iTunes data, it can update itself and output the regenerated JSON.
if let url = NSURL(string:"http://itunes.apple.com/search?term=b12&limit=40"),
data = NSData(contentsOfURL: url),
parsedJSON = JSONParser.parseDictionary(data),
iTD = iTunesData(dict: parsedJSON)
{
let tracks = map(iTD.results, {x in Track(dict:x.jsonDict)})
}
This is one of the things that I have been most interested in maintaining. I don’t know whether the iTunes data type is the best place for that to happen, or whether that should happen within the track. I am still working on this and developing it. Here’s a closer look at what the tracks
type is. It takes the JSONDictionary
from the iTunes type, and fills out its values to enable us to do this sort of round-tripping. We take JSON in, parse it, shape it into an iTunesData, and then spit it back out. If you have any suggestions, I’d be happy to hear them!
Thanks!
Anthony has since released a fully formed JSON Parser, called Aldwych. You can view and contribute on GitHub.
You can read more about Anthony’s Swift experiments, including his writing about JSON, on his blog.
Receive news and updates from Realm straight to your inbox