Fetching and parsing JSON with Swift / by Jake MacMullin

I'm really excited about Apple's new programming language and keen to learn as much about it as I can. I've found that the best way for me to learn a new language is to use it to build something. So I've started working on a new app (albeit a small one) using Swift.

One of the things this app needs to do is to interact with a web service. If I was writing this app in Objective-C I'd probably default to using AFNetworking for client/server communications and perhaps something like Mantle to simplify the process of converting the JSON returned from the server into model objects I can use in the app. However, as I'm using this project as an opportunity to learn about Swift I'm not sure it makes sense to rely too heavily on 3rd party libraries. The main thing I'm hoping to learn is how to solve problems the 'Swift way' (if there is such a thing). I'm worried that if I stick to using the libraries I'm familiar with all I'll end up learning is a new syntax and I'll miss out on the opportunity to think about how Swift's distinctive language features might let me solve problems in new ways. So I started out using NSURLSession and NSJSONSerialization directly to request JSON from the web service and parse it.

The app I'm working on has a pretty simple domain model. It's an app that curators of art galleries will be using to photograph art works in order to submit images of the works to a natural image recognition service. A separate app will let visitors to the galleries find out information about art works by pointing their phone's camera at the works. The domain model consists of locations which house one or more collections which consist of one or more artworks. The web service exposes a RESTful API that can be used to request information about these three entities.

A naive start

My naive first attempt at requesting and parsing the JSON looked a bit like this:

I created an NSURLSessionDataTask with a (trailing) completion closure that used NSJSONSerialization to parse the JSON that was returned by the API. This worked fine. Then I wrote more or less the same code for collections and artworks. The problem was that I found I was repeating myself a lot. I wondered if there was a way to refactor this code so that I wasn't repeating myself.

The repeated pattern

Each of the methods I'd written to handle locationscollections and artworks was doing something very similar.

First it was requesting data from a URL.

Then it was using NSJSONSerialization to parse the response in to a arrays or a dictionary.

Then it was pulling values out of the dictionaries and populating model objects.

Finally, it was calling a completion closure with the model objects.

The things that differed

The only things that differed between each of my methods to request different things was:

the URL the data was being requested from,

whether or not I was expecting one thing or many things back,

the keys I used to pull values from the dictionaries and the properties I wanted to set on my model objects.

A possible solution?

Well, I could easily pass in the URL to request the data from and somehow indicate if I expected one or many things back, but how could I specify which keys to use and which properties to set?

My first thought was that I might be able to do this with reflection. Was there a way I could annotate the properties on my model classes to indicate the keys that should be used for those properties in the JSON? Something like GSON for Android.

Eg:

@JSONKey("_id") var identifier: String

@JSONKey("loc_name" var name: String

No. Unfortunately, Swift doesn't have extensible attributes. There are some built-in to the language (such as @IBOutlet and @IBAction) but no mechanism for me to define my own.

Another possibility?

Ok, perhaps I could write a protocol that all my model classes would conform to that would provide this information about which keys map to which properties. How would I use this protocol though? Assuming I knew the mapping between keys and properties, how would I actually set the properties? How could I write code that would take a property name (as a String) and a value to set (extracted from the dictionary) and actually set the value of the property with that name. Does Swift have anything that lets me dynamically call a selector?

No, it doesn't seem to support this in 'pure-Swift'. I could ensure all my model classes extend NSObject and then I'd get this behaviour from NSObject but this doesn't feel like the 'Swift way'. It's not safe. There'd be no way for the compiler to check to make sure my mappings made sense. Given that I'm attempting to learn how to approach problems in a different way I decided against this approach and instead opted to keep trying to find another way.

Another way

So I decided to create a protocol to require all my model objects to implement an initializer that could take a dictionary. 

protocol Serializable {
    init(dictionary: [NSObject: AnyObject])
}

That way each class could specify the mappings between a dictionary and their properties in this initialiser. For example, the initializer for my Location class looks like this (I'm not sure if I'm casting correctly here):

    init(dictionary: [NSObject: AnyObject]) {
        if let nameObject: AnyObject = dictionary["name"] {
            if let nameString = nameObject as? String {
                self.name = nameString
            }            
        }
        if let identifierObject: AnyObject = dictionary["_id"] {
            if let identifierString = identifierObject as? String {
                self.identifier = identifierString
            }
        }
    }

A generic approach

So now I've got a way to specify the URL that the JSON should be requested from and a way of specifying the mapping between JSON keys and properties of a model object, how can I write some reusable code that can request data from some URL and parse it in to the right type of model object? This is just the problem generics are designed to solve.

I broke this down in to three parts:

Assuming I have a dictionary and I know the type of model object I want to map it to, I wrote a simple generic method that would call the initialiser of the right model type with the dictionary.

    func parseDictionaryToType<T: Serializable>(dictionary: [NSObject: AnyObject]) -> T {
        let parsedObject = T(dictionary: dictionary)
        return parsedObject
    }

This method has a placeholder type name 'T: Serializable'. This says that it will return an object of type 'T' where 'T' is a class that conforms to the 'Serializable' protocol. The body of the method is trivial. It simply calls the initializer of type 'T' that takes a dictionary. I know I can call this, because the placeholder type I used specified that 'T' must conform to 'Serlializable' and it defines that all conforming classes must have such an initializer.

To call this method all I need to do is:

let parsedObject: Location = self.parseDictionaryToType(dictionary)

It actually took me a while to figure this out! I was used to Java's approach to generics where you have to explicitly indicate the Type as an argument to a generic method with something like self.parseDictionaryToType<Location>(dictionary). It took me a while to figure out that Swift wants to infer this. By indicating that I want to store the result of this method call in a variable of type Location, Swift can infer what type to use in the body of the generic method.

Once I had a generic method that could convert a dictionary to an instance of the right type, it was simple enough to write one that could handle arrays of dictionaries:

    func parseArrayToArrayOfType<T: Serializable>(array: [AnyObject]) -> [T] {
        var parsedArray = [T]()
        for object in array {
            let dictionary = object as [NSObject: AnyObject]
            let parsedObject: T = self.parseDictionaryToType(dictionary)
            parsedArray.append(parsedObject)
        }
        return parsedArray
    }

(Although I couldn't figure out how to do this using a map function instead of the for each! I couldn't figure out how to write a map function that would take an array of AnyObject and return an array of T. If you have figured this out, please let me know in the comments below!)

Putting it all together

Finally, all I needed to do was to link all the pieces together. A method that takes a URL, and a completion closure that expects an array of the appropriate type:

    func getList<T: Serializable>(url: NSURL, completion:([T])->Void) {
        var parsedList = [T]()
        let request = NSURLRequest(URL: url)
        let urlSession = NSURLSession.sharedSession()
        let dataTask = urlSession.dataTaskWithRequest(request) {
            (var data, var response, var error) in
            if (!error) {
                var parseError : NSError?
                let unparsedArray = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &parseError) as [AnyObject]
                parsedList = self.parseArrayToArrayOfType(unparsedArray)
            } else {
                println(error)
            }
            // call the completion function (on the main thread)
            dispatch_async(dispatch_get_main_queue()) {
                completion(parsedList)
            }
        }
        dataTask.resume()
    }

And then finally, I could write my three methods to request locations, collections and artworks without repeating myself! Here's the one to request locations, the rest are left as an exercise for the reader.

    func getLocations(completion:([Location])->Void) {
        let locationsURL = NSURL(string: locationsEndPoint)
        self.getList(locationsURL, completion: completion)
    }

So much more to learn

Obviously, I've got a long way to go before I'm as comfortable using Swift as I am using Objective-C. But diving in and using Swift for a real project has been a great way to get started so far. I'm really looking forward to learning more about how this language will let me approach problems in different ways. If you've also been using Swift to parse JSON I'd love to hear a bit about how you've approached it.