Using Codable Protocol in Swift 4.1

Apple introduced a Codable protocol in Swift 4 - A new way to encode/decode data to/from model objects. This new addition allows developers to make data types encodable and decodable thus allowing conversion to and from external JSON data.

Earlier, when developers wanted to perform decoding and encoding without built-in support, they would either needed to do it manually or use a 3rd party library to perform such operations. With Swift standard library defining a standardized approach to data encoding and decoding, it has become much easier to handle conversion from local model objects to JSON and vice-versa.

To support both encoding and decoding, all you have to do it to declare conformance to Codable, which combines the Encodable and Decodable protocols. This process is known as making your type Codable.

As evident from Swift open source code, a Codable type is a type which conforms to both Decodable and Encodable protocols.


/// A type that can convert itself into 
/// and out of an external representation.
public typealias Codable = Decodable & Encodable

It is important to note that it is not mandatory to use Codable every time for all the use-cases unless your requirement is to both serialize and de-serialize JSON response.

For example, if your requirement is only to send request with JSON payload to server, you can make your struct to conform to just Encodable protocol. On the other hand, if you want to download JSON, and convert it into model objects, you might want to go with Decodable protocol implementation

Let's start with examples similar to the real-world use-case. Starting with easier moving on to more complicated use-cases.

  1. Simple Dictionary

Let's say, we want to download and decode the simple dictionary representation as follows,

Employee:
    name - String
    age - Int
    address - String

Here is our corresponding JSON data,

{"name": "abc", "age": 40, "address": "USA"}

Let's start making a model object struct conforming to Codable protocol capable of storing all the properties from JSON data


struct Employee: Decodable {

    let name: String
    let age: Int
    let address: String

}

And then write a code to read JSON data and directly get the model object without further complications,

let employeeJSON = """
                        {"name": "abc", "age": 40, "address": "USA"}
                    """
// Converting raw JSON string into Data type                    
let jsonData = employeeJSON.data(using: .utf8)!

do {
    let decodedData = try JSONDecoder().decode(Employee.self, from: jsonData)
    // Prints
    // Employee(name: "abc", age: 40, address: "USA")

    print(decodedData)
} catch {
    print(error.localizedDescription)
}

2. What if my JSON keys are different?

This is a very valid question. If your JSON keys are different than the variable names associated with model object, Codable implementation will get confused and will fail to map these values. In order to solve this problem, you can define custom mapping with the help of CodingKeys protocol and define your custom keys there.

For example if you JSON looks like this,

{"employee_name": "abc", "employee_age": 40, "employee_address": "USA"}

This is how you can write code for your Employee Codable object

struct Employee: Decodable {

    let name: String
    let age: Int
    let address: String

    enum CodingKeys: String, CodingKey {
        case name = "employee_name"
        case age = "employee_age"
        case address = "employee_address"
    }
}

Here, we're using coding keys to be able to directly map from JSON keys to object model keys. In terms of decoding, nothing changes. You can still decode JSON values the way we did in previous example,

let employeeJSON = """
                        {"employee_name": "abc", "employee_age": 40, "employee_address": "USA"}
                    """
let jsonData = employeeJSON.data(using: .utf8)!

do {
    let decodedData = try JSONDecoder().decode(Employee.self, from: jsonData)
        
    // Prints
    // Employee(name: "abc", age: 40, address: "USA")
    
    print(decodedData)
} catch {
    print(error.localizedDescription)
}

3. Custom Decoding strategy and key mapping support in Swift 4.1

By default Codable uses keyDecodingStrategy as .useDefaultKeys for decoding from JSON to model objects. This maps exact keys from JSON representation to the one described in your model object - As we saw in previous two examples.

But what if your JSON keys are represented in snake case and you want to corresponding mapping from snake case to camel case of your model object? Like,

employee_name (JSON) to employeeName (Model object)

Swift 4.1 added a support to customize the keyDecodingStrategy value associated with JSONDecoder or JSONEncoder object. If you simple want to map my_key from JSON to myKey in your model object, simple set keyDecodingStrategy to convertFromSnakeCase.

Let's look at the example. Let's see our incoming JSON looks like this,

{"employee_name": "abc", "employee_age": 40, "employee_address": "USA"}

In order to perform mapping from snake case to corresponding camel case in Swift 4.0, you might have done something like this,

struct Employee: Decodable {

    let employeeName: String
    let employeeAge: Int
    let employeeAddress: String

    enum CodingKeys: String, CodingKey {
        case employeeName = "employee_name"
        case employeeAge = "employee_age"
        case employeeAddress = "employee_address"
    }
}

And it would work. But it's a lot of code. Let's try to simplify it with our custom decoding strategy,

let employeeJSON = """
                {"employee_name": "abc", "employee_age": 40, "employee_address": "USA"}
            """
let jsonData = employeeJSON.data(using: .utf8)!

do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let decodedData = try decoder.decode(Employee.self, from: jsonData)
    
    // Prints
    // Employee(employeeName: "abc", employeeAge: 40, employeeAddress: "USA")
    
    print(decodedData)
} catch {
    print(error.localizedDescription)
}

Which further simplifies our object model representation allowing us to take out CodingKey implementation. The simplified and compact Employee struct now looks like this,

struct Employee: Decodable {
    let employeeName: String
    let employeeAge: Int
    let employeeAddress: String
}

4. Flattening nested JSON

There are certain use cases where you might want to flatten the nested JSON into simple model object. Support your JSON looks like a nested structure described below,

{"info": { "style": "kolsch", "abv": "4.9" }, "name": "lawnmower"}

Instead of adding one more level of nesting, what if we want to represent this JSON with three properties at the top level?

  • style
  • abv
  • name

Let's take a look at the full decoding and encoding structure and I will add further explanation next to it,

struct User: Codable {

    var name: String
    var style: String
    var abv: String

    enum InfoKey: String, CodingKey {
        case style
        case abv
    }

    enum CodingKeys: String, CodingKey {
        case name
        case info
    }

    // JSON to model object
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        let info = try values.nestedContainer(keyedBy: InfoKey.self, forKey: .info)
        style = try info.decode(String.self, forKey: .style)
        abv = try info.decode(String.self, forKey: .abv)
    }

    // Model object to JSON 
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        var infoValue = container.nestedContainer(keyedBy: InfoKey.self, forKey: .info)
        try infoValue.encode(style, forKey: .style)
        try infoValue.encode(abv, forKey: .abv)
    }
}

First off, we define a User struct with three base properties we want to extract. Then we define two enums within this original struct.

First enum represents name and nested info object. We go ahead and start defining one more enum InfoKey to be able to decode values nested in info key.

Please note that since we're manually flattening the JSON, we need to implement init(from decoder: Decoder) method to have the full control on decoding operation. First, we decode the top level key name, then get the inner nested object into intermediate info object.

We could keep it as is, but what we want to do here is flatten the original structure. Thus in the next step, we flatten it into individual properties as follows,

let info = try values.nestedContainer(keyedBy: InfoKey.self, forKey: .info)
style = try info.decode(String.self, forKey: .style)
abv = try info.decode(String.self, forKey: .abv)

In the similar way, we can write our encoder to convert from model object to equivalent JSON representation,

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    var infoValue = container.nestedContainer(keyedBy: InfoKey.self, forKey: .info)
    try infoValue.encode(style, forKey: .style)
    try infoValue.encode(abv, forKey: .abv)
}

So now, moving on to actual example, if I want to decode JSON like following this is what I need to do,


let nestedData = """
             {"info": { "style": "kolsch", "abv": "4.9" }, "name": "lawnmower"}
           """
do {
    let decodedUserObject = try JSONDecoder().decode(User.self, from: nestedData.data(using: .utf8)!)
    
    // Prints
    // User(name: "lawnmower", style: "kolsch", abv: "4.9")
    
    print(decodedUserObject)
    if let encodedUserObject = try? JSONEncoder().encode(decodedUserObject) {
        
        // Prints
        // {"name":"lawnmower","info":{"style":"kolsch","abv":"4.9"}}
        
        print(String(data: encodedUserObject,encoding: .utf8)!)
    }
} catch let error {
    print(error.localizedDescription)
}

5. Decoding Array of Dictionaries

So far we saw how to decode dictionaries, and ways to flatten them. Now it's the time to see how to decode simple Swift arrays. Just like previous examples, let's take a look at JSON first,

[
    {"first": "Tom", "last": "Smith", "user_age": 31},
    {"first": "Bob", "last": "Smith", "user_age": 28}
]

Since this JSON represents an array of dictionaries with similar structure, we will first create a simple structure matching this JSON and confirming to Codable.

struct Human: Codable {
    let firstName: String
    let lastName: String
    let age: Int

    enum CodingKeys: String, CodingKey {
        case firstName = "first"
        case lastName = "last"
        case age = "user_age"
    }
}
Please note how we used custom CodingKeys since JSON keys do not exactly match with and map to property names for Human struct. For example, user_age key in JSON gets mapped to age in Codable structure and so on...

Decoding JSON containing array of dictionaries has similar format as decoding individual dictionaries with a small difference that we will be putting the desired object type inside square brackets (e.g. []).

We will see how simple the code is to convert array of dictionaries into corresponding array of Codable objects.

let jsonData = """
[
    {"first": "Tom", "last": "Smith", "user_age": 31},
    {"first": "Bob", "last": "Smith", "user_age": 28}
]
""".data(using: .utf8)!

do {
    let decodedData = try JSONDecoder().decode([Human].self, from: jsonData)
    
    // Prints
    // [
    // CodableExercise.Human(firstName: "Tom", lastName: "Smith", age: 31), 
    // CodableExercise.Human(firstName: "Bob", lastName: "Smith", age: 28)
    // ]
    
    print(decodedData)
} catch {
    print(error.localizedDescription)
}

It is that simple. No more magic tricks, no more complicated mappings and hassle dealing with annoying 3rd party libraries.

6. Decoding Arrays and Dictionaries together

Having seen examples of decoding dictionaries and arrays, now it the time to see how we can decode hybrid JSON containing at least one array and one dictionary. Here's the JSON we are going to use,

{
    "breweries": [
        {"name": "ddd", "location": "Seattle"}, {"name": "ddwewe", "location": "Washington"}
    ],
    "pageDetails": {"page": 1, "page_count": 100}
}                              

Here we will have to create a top level object called Drink with two properties nested under it,

Drink
.   - pageDetails
.   - breweries

First, let start by creating smaller struct PageDetails to store drink details

struct PageDetails: Codable {
    let page: Int
    let pageCount: Int

    enum CodingKeys: String, CodingKey {
        case page
        case pageCount = "page_count"
    }
}
Here again, we had to use custom CodingKeys, since at least one key in JSON, page_count does not exactly match to property name page associated with the model object

Similarly, let's start creating another Codable conforming struct named Brewery holding two properties, namely name and location.

struct Brewery: Codable {
    let name: String
    let location: String
}

Here is the special case since key names in both dictionary and struct exactly match, so we don't need to implement CodingKeys protocol.

Once we created child models, now it the time to start creating top level object - Let's call it a Beer

Since our JSON contains two keys, first represents a dictionary, while second represents an array of dictionaries, the Beer struct conforming to Codable will look like this,

struct Beer: Codable {
    let pageDetails: PageDetails
    let breweries: [Brewery]
}

Congratulations! We successfully created Codable structure for JSON with medium complexity level. Now, if you wish to decode the JSON into corresponding model object Beer, the following code will help you do it:

let beersJSON = """
                        {"pageDetails": {"page": 1, "page_count": 100}, "breweries": [{"name": "ddd", "location": "Seattle"}, {"name": "ddwewe", "location": "Washington"}]}
                      """

do {
    let decodedBeerObject = try JSONDecoder().decode(Beer.self, from: breweriesJSON.data(using: .utf8)!)
    
    // Prints
    // Beer(
    //  pageDetails: CodableExercise.PageDetails(page: 51, pageCount: 500), 
    //  breweries: [
    //     CodableExercise.Breweries(name: "ddd", location: "Seattle"), 
    //     CodableExercise.Breweries(name: "ddwewe", location: "Washington")
    //    ]
    //  )
    
    print(decodedBeerObject)
} catch let error {
    print(error.localizedDescription)
}

7. Handling nested JSON dictionary

Now we will see slightly trickier situation. I was stumped by it for some time until I realized the solution was in fact very simple. Imagine an incoming JSON with nested dictionary structure like this,

let jsonDictionary = """
    {"tom": {"first": "Tom", "last": "Smith", "user_age": 31},
    "bob": {"first": "Bob", "last": "Marley", "user_age": 18},
    "peter": {"first": "Peter", "last": "Pan", "user_age": 8}}
"""

Now if you try to implement it with one of the rules we saw earlier, you will notice that none of them actually apply here. You can probably try to add the one with dictionary decoding, but what about multiple keys and one dictionary for each of them then?

To begin with, let's at least start creating a human struct first,

struct Human: Codable {
    let firstName: String
    let lastName: String
    let age: Int

    enum CodingKeys: String, CodingKey {
        case firstName = "first"
        case lastName = "last"
        case age = "user_age"
    }
}

So we simply assume that we want to decode this JSON into dictionaries where key is a string and value is a Human object. Once we have this realization, decoding logic for this JSON structure becomes relatively easy to figure out,

let jsonDictionary = """
    {"tom": {"first": "Tom", "last": "Smith", "user_age": 31},
    "bob": {"first": "Bob", "last": "Marley", "user_age": 18},
    "peter": {"first": "Peter", "last": "Pan", "user_age": 8}}
"""
try {
    let decodedDictionary = try JSONDecoder().decode([String: Human].self, from: jsonDictionary.data(using: .utf8)!)
    
    // Prints
    // [
    // "tom": CodableExercise.Human(firstName: "Tom", lastName: "Smith", age: 31), 
    // "peter": CodableExercise.Human(firstName: "Peter", lastName: "Pan", age: 8), 
    // "bob": CodableExercise.Human(firstName: "Bob", lastName: "Marley", age: 18)
    // ]
    
    print(decodedDictionary)
} catch {
    print(error.localizedDescription)
}

Which are exactly same values as we just read from JSON data.

Looks like this post is getting too long, even longer than I originally decided to write, so I will stop here. In the interest of time and length of this post, I will spin off  couple of more posts about automatic data conversion while decoding/encoding and handling default values and exceptions while creating Codable objects.

Update

I have added a code sample on Github. You can play with existing examples or improve it by including additional mappings. I am excited to see what you are going to do with incredible Codable protocol in your next app.

Acknowledgements:

I am thankful to several people and blogs which helped me make this tutorial. My special thanks to Ben Scheirman for the awesome post on codable protocols in Swift 4. This should be enough as long as basic and intermediate knowledge of Codable is concerned. If you have any suggestion or input that can be used to improve this article, please let me know. You can send me an email or Tweet to me @jayeshkawli on Twitter

References: