My book on iOS interface design, Design Teardowns: Step-by-step iOS interface design walkthroughs is now available!

JSON and Swift

Many have tried to tackle the issue and there're now some really good solutions. Here are just a handful of them and there are 2 main categories.

Some solutions make it really easy to make sense of JSON data. This is significant because Swift's strong type-safety makes it hard to work with JSON without much explicit casting:

Others go a step further and help make it easy to work with actual data models. They help to decode JSON strings or objects into native types (read Structs or Classes):

After a brief survey of available options, it felt like something was missing. We didn't have a complete solution that made the round trip - one that is able to produce native types from JSON and also JSON from native types.

Part of the reason, I suspected was the difficulty of extending types of data models, of which value types (read Structs) are becoming increasingly popular. This, however, changed in Swift 2.0 with the ability to write extensions on protocols.

So here's my take on the problem - JSONCodable.

It's by no means perfect - but I promise it will keep evolving as we discover better means of working with Swift.

Here's a quick run through of its features:

  • It provides capabilities for encoding and decoding JSON
  • It leverages features to expose an API as Swift-like as possible
  • No operators were harmed in the making of this project

Still here? Let's take a look at how it works (you can also find this information on Github but I've tried to provide reasons for some of the implementation details here.)

JSONCodable is made of two seperate protocols JSONEncodable and JSONDecodable.

JSONEncodable generates Dictionarys (compatible with NSJSONSerialization) and Strings from your types while JSONDecodable creates structs (or classes) from compatible Dictionarys (from an incoming network request for instance.)

Suppose we have the following data models:

struct User {  
    var id: Int = 0
    var name: String = ""
    var email: String?
    var company: Company?
    var friends: [User] = []
}

struct Company {  
    var name: String = ""
    var address: String?
}

Encoding/Serialization

To encode JSON, we'll add conformance to JSONEncodable. You may also add conformance to JSONCodable.

extension User: JSONEncodable {  
    func JSONEncode() throws -> AnyObject {
        var result: [String: AnyObject] = [:]
        try result.archive(id, key: "id")
        try result.archive(name, key: "full_name")
        try result.archive(email, key: "email")
        try result.archive(company, key: "company")
        try result.archive(friends, key: "friends")
        return result
    }
}

extension Company: JSONEncodable {}  

The default implementation of func JSONEncode() inspects the properties of your type using reflection (see Company.) If you need a different mapping, you can provide your own implementation (see User.)

Using protocol extensions we can provide a default implementation that seems reasonable! That's great.


Decoding/Deserialization

To decode JSON, we'll add conformance to JSONDecodable. You may also add conformance to JSONCodable.

extension User: JSONDecodable {  
    mutating func JSONDecode(JSONDictionary: [String : AnyObject]) {
        JSONDictionary.restore(&id, key: "id")
        JSONDictionary.restore(&name, key: "full_name")
        JSONDictionary.restore(&email, key: "email")
        JSONDictionary.restore(&company, key: "company")
        JSONDictionary.restore(&friends, key: "friends")
    }
}

extension Company: JSONDecodable {  
    mutating func JSONDecode(JSONDictionary: [String : AnyObject]) {
        JSONDictionary.restore(&name, key: "name")
        JSONDictionary.restore(&address, key: "address")
    }
}

Unlike in JSONEncodable, you must provide the implementations for func JSONDecode(). As before, you can use this to configure the mapping between keys in the Dictionary to properties in your Structs and Classes.

We can't provide a default implementation of this because there's no way in Swift to dynamically set property values at runtime. The best we can do is use inout parameters (some have tried to obfuscate this using operators - Sigh)

Limitations

  • Your types must be initializable without any parameters, i.e. implement init(). You can do this by either providing a default value for all your properties or implement init() directly and configuring your properties at initialization.

  • You must use var instead of let when declaring properties.

JSONDecodable needs to be able to create new instances of your types and set their values thereafter. So we need to a standard way to initialize these types via init() and the properties must be mutable to allow setting their values.


What's next?

  • Ideally we'd allow for properties to be constant - which really might make sense for some situations. However, we then need some way of performing a complete initialization of the type (which introduces some ugliness in the API because consumers need to provide a non-trivial initializer.)

  • More symmetry in the API regarding errors when encoding and decoding JSON.

If you have any thoughts, let me know here or on twitter @matthewcheok!