Skip to main content

Updating Aldwych (Part 1): Parsing JSON with recursive enums in Swift


The purpose of Aldwych from the very beginning has been to provide JSON parsing that is type safe, flexible and familiar. And when recursive enums were first announced by Chris Lattner at WWDC 2015, I hoped they would enable a great internal simplification of the JSON parser.

Step One

With this hope in mind, and with a handkerchief filled with sandwiches tied to a stick, I decided to experiment with recursive enums by making the core type in Aldwych simply this:
public enum JSONValue {
    
    case JString(String), Number(NSNumber), JBool(Bool), Null
    indirect case JArray([JSONValue]), JDictionary([String:JSONValue])
    
}
which was a pretty good start but I encountered one very particular problem: the for-in loop. If you think about an Array we loop through it using for v in array {} but in a Dictionary we use for (k,v) in dictionary {}. This means that the Element type of an Array is a single value while that of a Dictionary is a tuple.

This was the first hurdle because a GeneratorType (which is required for adding for-in capabilities to a type) isn't able to return a JSONValue when a JSONValue of JArray is being iterated over but a (String, JSONValue) tuple when a JDictionary is being iterated over.

It seemed tantalising that there might be a "where case" type statement that I could add to an extension of the type or (a protocol that the type adhered to) but sadly I found none at present. (Although I'd be very glad to hear if there is some way of doing this, because it would save repetition of code.)

So I had two options I could see: one was to expect anyone implementing the library to use for (_,v) in array {} and the other was to stick with the aim of familiarity and push for the typical form of for v in array {}.

Solution

I thought about a number of approaches to solve this problem and settled for that which I thought was the least complicated. It was to create two additional types:
public enum JSONArray:JSONObjectType {
    case JArray([JSONValue])
}
and
public enum JSONDictionary:JSONObjectType {
    case JDictionary([String:JSONValue])
}
These behave as top-level wrappers for the JSON while the JSONValue type retains its own array and dictionary types:
indirect case JArray([JSONValue]), JDictionary([String:JSONValue])
This might seem crazily repetitious but it means that all arrays and dictionaries are of a single type and we don't need to wrestle with figuring out how a JArray([JSONValue]) is going to also house values of type JSONArray and JSONDictionary, which would add an even deeper layer of wrapping and unwrapping that I can't even start to get my head around.

Looking forward

With the main types set I was able to pretty much lose all the code I used to have inside Aldwych for keeping arrays and dictionaries working internally while segregating types. But what I'd also hoped to do was leverage protocols in amazing new ways to remove even more of the repeated code across types. Instead I've ended up using protocols simply as a way to keep consistency between the JSONValue Array and Dictionary types and the JSONArray and JSONDictionary types.

This hasn't been quite so satisfying and I'm left hoping for further extensions of the where clause in Swift as well as other refinements so that I can distinguish more intelligently between cases. But for now I'm simply primed for future additions to Swift.

Reliability first, whizz bang second

I found while writing and amending the old code that some of the stuff that I wanted to add made things buggier and more fussy than I wanted. For example: I've seen other parsers use the Swift.String and Swift.Bool, etc. trick to enable enum cases to be labelled simply .String and .Bool and so on. But not only was it a bit of a pain to remember but also I hit some situations where I needed to typealias Swift.String to make it work, or it just didn't work with certain methods. (Mapping a dictionary of keys to an array was one instance if I remember correctly.)

I also wanted to get stuck into if-case and for-case but found switch more reliable for suiting my needs at present.

Conclusion

There's plenty more to write about the process of making Aldwych work with recursive enums and I also want to keep refining and making it even easier and more familiar to use within the context of Swift. (And at the same time to write documentation and tests.)

For now I'll leave you with the playground on GitHub and ask you to kindly feedback, share innovative code and generally enjoy exploring it. I think you'll particularly like how easy it is to edit and subset data, which can easily be transformed straight back into JSON data for saving to a file or uploading to the web.


Endorse on Coderwall

Comments