Weekly Swift articles, podcasts and tips by John Sundell.

Codable

Published on 14 Jan 2019

Introduced in Swift 4, the Codable API enables us to leverage the compiler in order to generate much of the code needed to encode and decode data to/from a serialized format, like JSON.

Codable is actually a type alias that combines two protocols — Encodable and Decodable — into one. By conforming to either of those protocols when declaring a type, the compiler will attempt to automatically synthesize the code required to encode or decode an instance of that type, which will work as long as we’re only using stored properties that themselves are encodable/decodable — such as when defining this User type:

struct User: Codable {
    var name: String
    var age: Int
}

By only adding that single protocol conformance, we’re now able to encode a user instance into JSON Data by using a JSONEncoder:

do {
    let user = User(name: "John", age: 31)
    let encoder = JSONEncoder()
    let data = try encoder.encode(user)
} catch {
    print("Whoops, an error occured: \(error)")
}

Since encoding a value is an operation that could potentially throw an error, we need to call encode() prefixed by the try keyword, and handle any error that was thrown. In the above example, we encapsulate our encoding code in a do block, and use catch to catch any error that was encountered.

Now that we have encoded a value into data, let’s see if we can decode it back into a User instance. To do that, we use a JSONDecoder, by passing it our data while telling it what type we’re interested in decoding:

let decoder = JSONDecoder()
let secondUser = try decoder.decode(User.self, from: data)

Our secondUser should now be exactly the same as our original user, and we’ve successfully completed an encoding/decoding roundtrip! 🎉

However, sometimes the JSON format we’re dealing with doesn’t exactly match the structure that want our corresponding Swift type to have. For example, the JSON representing one of our User values might look like this:

{
    "user_data": {
        "full_name": "John Sundell",
        "user_age": 31
    }
}

There are a couple of different paths we can take in a situation like this. One option is to simply make our User type match the above JSON structure — but that’d make it much harder to use, and doing so would make it look quite out of place among other Swift code (since it’d have to use property names like full_name). Another option would be to manually implement our type’s conformance to Codable, but that’d require quite a lot of extra code.

The good news is that there’s a third way — and that’s introducing a new type that we’ll specifically use for encoding and decoding. That way we can make that type use the same structure as our JSON does, without affecting our actual model code. Here’s what such a type could look like for the above JSON format:

extension User {
    struct CodingData: Codable {
        struct Container: Codable {
            var fullName: String
            var userAge: Int
        }

        var userData: Container
    }
}

We’ll then add a convenience method to our new User.CodingData type, that’ll let us quickly convert a decoded value into a proper User instance, like this:

extension User.CodingData {
    var user: User {
        return User(
            name: userData.fullName,
            age: userData.userAge
        )
    }
}

While our new type does match the structure of our JSON, the keys still don’t exactly match (fullName vs full_name) — but thankfully, both JSONEncoder and JSONDecoder provide a way to solve that for us — by setting their keyDecodingStrategy or .keyEncodingStrategy to .convertToSnakeCase. Combining our new type with that capability, here’s how we can now decode a User value, without having to make any modifications to our original type:

// We'll use the "convertFromSnakeCase" key decoding strategy to
// automatically convert the snake_case keys in our JSON data into
// camelCase, which we're using in our Swift code.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

// Rather than decoding a User value directly, we instead decode an
// instance of User.CodingData, and then convert that into a user.
let codingData = try decoder.decode(User.CodingData.self, from: data)
let user = codingData.user

Using a specific type for encoding and decoding, when our original type has a completely different structure than our JSON, can be a great way to bridge the gap between serialized data and our Swift code — while still being able to take full advantage of compiler-generated implementations of Codable.

Of course, many times we won’t need to do that, especially if we’re just encoding and decoding to and from local data — in those cases we can just use Codable as it is, without the need for any additional boilerplate.

Thanks for reading! 🚀