Better JSON parsing in Swift

Some time after this post was published Apple released Swift 4, which includes Codable.

Every since Swift came out, the ObjectMapper library has been the go-to tool for many people's JSON parsing needs, including me. It has been a great tool, especially during the transition from Objective-C to Swift as we were all still figuring out what works well for Swift and what doesn't. One of the reasons ObjectMapper has served people so well is because it's pretty similar to tools that were available for Objective-C at the time Swift was introduced.

ObjectMapper has been a huge productivity booster. We should all be thankful that there are so many incredible developers in our community that build awesome things that to make our work easier. This post is in no way intended to diminish the library or the efforts of its contributors in any way. It's a great tool and if it suits your needs, you should definitely keep using it.

However, for the last year and a half or so, I've been experiencing a few issues* with ObjectMapper, which has spawned a recurring discussion at the company I work for as to whether or not we should still be (blindly) using ObjectMapper as our tool of choice. My position is that we should look into other libraries and solutions, mainly for the following reasons:

  • With ObjectMapper, mapping happens after object initialization, which means that you have to declare all your properties as mutable optional variables, unless you can resort to a sane default value (which isn't a viable option most of the time, and you still have to declare it as a variable instead of a constant).
  • Because all your properties are defined as optionals, mapping will still succeed if you feed the mapper any arbitrary piece of JSON data. You can implement the failable convenience initializer and have it fail if one of more fields are missing in the input dictionary, but it still doesn't save you from having to use optionals everywhere.
  • Because mapping cannot fail, unless you feed it an invalid input type, ObjectMapper cannot report mapping errors like a missing key or a type mismatch. In practice, this means error handling is moved from the model layer to the layer that consumes the model layer and is reproduced many times over.
  • In my personal opinion, the model definition should reflect the actual requirements as close as possible. I.e.: if your object isn't consumable without an identifier and/or title, its properties should not be defined as optional and mapping should fail if its input value is missing. If it fails, you should be handed the reason for failure, which makes it possible to determine where the error lies.
  • ObjectMapper doesn't play well with structs. It does work with structs, but only structs that use mutable optional variables and define methods as mutating, so it's not possible to map to immutable structs, which often is more pleasant and safer to work with.

Alternatives to ObjectMapper

So, given these issues (which may not seem like issues to some), what are the alternatives? Despite Swift only being three years old - which is still young for a programming language - there are plenty of other tools to choose from. I won't go through all of them here, but here are a few libraries for your consideration.

We'll be using the following simple struct as an example:

struct Article {  
    let guid: Int64
    let title: String
    let body: String
    let author: Author?
    let tags: [String]
}

Argo

This library by Thoughtbot uses a functional approach to JSON parsing. Personally, I'm a fan of this library because I'm a fan of the functional programming style. However, Argo does come with a few caveats you should be aware of:

  • It has a couple dependencies, like Runes and (to some extent) Curry. This isn't an issue by itself, but it does link your app to two additional frameworks, which might affect your app's startup time.
  • It relies heavily on type inference, which may upset the compiler and force you to break up your expressions into smaller pieces.
  • It uses a lot of infix operators, which looks totally badass, but they also obscure what's actually going on. If you need to perform some additional steps inside your mapping, this may not be as straightforward.
  • According to some benchmarks, Argo is relatively slow with large chunks of data, though it's debatable if this will really be noticeable in the real world.

If you can live with these drawbacks, Argo is a great choice. As a bonus, Thoughtbot also offers a library called Swish, a very lightweight wrapper around URLSession, which lets you map define requests that map directly to your structs or objects.

Example
extension Article: Decodable {  
    static func decode(_ json: JSON) -> Decoded<Article> {
        return curry(self.init)
            <^> json <| ["guid"]
            <*> json <| ["title"]
            <*> json <| ["body"]
            <*> json <|? ["author"]     // optional
            <*> json <|| ["data.tags"]  // nested
    }
}

Marshal

Marshal is a fairly new library that seems to have been created with the idea of solving most (if not all) of the mentioned issues, without adding a whole lot of difficult-to-grok magic. As you can tell from the following example, it's as close to vanilla Swift you can get, without having to pull-and-cast values from a dictionary yourself.

Example
extension Article: Unmarshaling {  
    init(object: MarshaledObject) throws {
        guid = try object.value(for: "guid")
        title = try object.value(for: "title")
        body = try object.value(for: "body")
        author = try? object.value(for: "author")
        tags = try object.value(for: "data.tags")
    }
}

Mapper

This library by Lyft called Mapper is very similar to Marshal. It too uses a throwing initializer and doesn't add a whole lot of magic to the process.

Example
extension Article: Mappable {  
    init(map: Mapper) throws {
        guid = try map.from(for: "guid")
        title = try map.from(for: "title")
        body = try map.from(for: "body")
        author = try? map.from(for: "author")
        tags = try map.from(for: "data.tags")
    }
}

There are many others like, in no particular order, Gloss, Decodable, Genome and SwiftyJSON. For my personal projects, I've settled on Argo and Marshal, at least for now.

Erik van der Wal

Erik van der Wal

I love building things with Swift, Objective-C and Ruby and tinkering with technologies like Golang and Elixir.

comments powered by Disqus