Using Codable protocol in Swift 4

Apple introduced a Codable protocol in Swift 4. This new addition allows developers to make data types encodable and decodable for compatibility with external representations such as JSON.

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 struct and properties 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 types 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 serialize and de-serialize JSON response. For example, your requirement is only to send request with JSON payload to server, you can make your struct to conform to just Encodable protocol.

Let's start with example similar to the real-world use-case. The structure follows below representation.

Home
 * people - Array of value of type Person
 * room - A value of type Room
 * car - Bool
 * buildDate - Date

Room
 * direction - Int
 * name - String
 * wall - A value of type Wall

Wall
 * color - Int
 * width - Int

Here, we will use the same example for JSON request as well as response. As mentioned above, A Home is a root object, which has sub-properties. Some of them are of primitive/in-built types and others such as Wall and Room are of custom type. These custom properties further have their own properties.

Let's start making structs for each of the properties mentioned above and let's make them conform to Codable protocol. In this case making them conform to Codable will make sure we will be able to both serialize and de-serialize request/response.

// MARK: Struct Home
struct Home: Codable {
    let people: [Person]
    let room: Room
    let car: Bool?
    let buildDate: Date

    enum CodingKeys: String, CodingKey {
        case people = "family"
        case room
        case car
        case buildDate = "build_date"
    }
}

// MARK: Struct Person
struct Person: Codable {
    let name: String
    let address: String
}

// MARK: Struct Room
struct Room: Codable {
    let direction: Int
    let name: String
    let wall: Wall

    enum CodingKeys: String, CodingKey {
        case direction = "room_direction"
        case name
        case wall
    }
}

// MARK: Struct Wall
struct Wall: Codable {
    let color: Int
    let width: Int
}
  • Please note in above example how we use CodingKeys enum which conforms to both CodingKey and String. This is useful when the property names in your struct are different from the one in underlying JSON.

    For example, in above mapping Struct home uses people as the property. However, JSON refers to it as a family. Hence we use CodingKeys to map property people to family.

  • Alternatively, if all your property names on struct match with thos in the JSON request/response, there is no need to use CodingKeys enum in your struct

  • JSON may or may not have certain properties defined in the struct. In that case, that property can be declared as an optional with ? symbol at the end. This will make sure JSON is parsed with success even if response is missing that property.

    If property is not declared as an optional and it is missing from the response, Codable will fail to de-serialize the response.

  • As evident from the above example, it is easy to nest one custom property into another

For the sake of completeness, let's also have a look at JSON we are going to use with this structure.

{
"family": 
        [{"name": "aaa", "address": "bbb"}, 
        {"name": "aaa1", "address": "bbb1"}], 
 "room": {
         "room_direction": 2, 
         "name": "Living Room", 
         "wall": {"color": 10, "width": 20}
         }, 
 "build_date": "10/29/2017"
 }

Let's start doing encoding and decoding activity using this JSON and made up structure.

To revise, Encoding is the action of converting native data model/properties/struct into JSON representation and Decoding is an exactly opposite operation - Converting incoming JSON representation into native data model/properties/struct

Let's say we have following Home object we want to serialize into JSON response to transmit over the network.

let home = Home(people: [Person(name: "aaa", address: "bbb"), Person(name: "aaa1", address: "bbb1")], room: Room(direction: 2, name: "Living Room", wall: Wall(color: 10, width: 20)), car: true, buildDate: Date())

// Make JSON encoder
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()

// EncodedHomeObject is of type Data which is ready to transmit over the network.
if let encodedHomeObject = try? jsonEncoder.encode(home) {
    // Prints the JSON representation of model object.
    print(String(data: encodedHomeObject,encoding: .utf8)!)
    
    // Decode the encodedHomeObject back into struct Home representations
    if let decodedHomeObject = try? jsonDecoder.decode(Home.self, from: encodedHomeObject) {
        print(decodedHomeObject)
    }
}

I have used try? in the above example, but if you want to catch and print error, you may put decoding/encoding operations in the try-catch block. Using try keyword and printing error has the advantage of using for debugging purpose. It can be used to find mapping errors.

do {
    let decodedHomeObject = try jsonDecoder.decode(Home.self, from: encodedHomeObject)
    print(decodedHomeObject)
} catch let error {
    print(error.localizedDescription)
}

Let's look at exactly opposite example. We have an incoming JSON and we want to de-serialize it into underlying struct. JSON looks like this,

let homeJSON = """
                {"family": 
                    [
                    {"name": "aaa", "address": "bbb"}, 
                    {"name": "aaa1", "address": "bbb1"}], 
                "room": 
                    {"room_direction": 2, "name": "Living Room", 
                "wall": {"color": 10, "width": 20}
                }, 
                "build_date": "10/29/2017"}
               """
               
if let decodedHomeObject = try? jsonDecoder.decode(Home.self, from: homeJSON.data(using: .utf8)!) {
    // Prints decodeHomeObject of type Home 
    print(decodedHomeObject)
    
    // Converts object of type Home back into JSON representation.
    if let encodedHomeObject = try? jsonEncoder.encode(decodedHomeObject) {
    
        // Prints original JSON representation
        print(String(data: encodedHomeObject,encoding: .utf8)!)
    }
}
               

Using Custom date formatters

It is often requirement to use dates in the app while sending requests and receiving responses. Codable protocol offers support for custom date formats. It can be achieved with following change.

let jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .iso8601

When custom date encoding format is used, Swift date Date() gets converted to following readable format when transformed into JSON. (Assuming today's date is 10/29/2017)

"2017-10-29T09:46:06Z"

If server is capable of handling date of iso8601 format, there is no need for client code to include additional logic for such conversion. Built-in JSONEncoder encoding strategy handles it transparently.

Similarly, custom date formatters can be used for decoding JSON response.

Let's say you are expecting a date with format "10/29/2017". JSONDecoder can be configured to handle date formats such as this.

let jsonDecoder = JSONDecoder()
let dateFormatter = DateFormatter()

// DateFormat is a variable value. You can change it as per requirements. More date formatters can be found at https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DataFormatting/Articles/dfDateFormatting10_4.html
dateFormatter.dateFormat = "MM/dd/yyyy"

jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)

When date in response is read as 10/29/2017, with custom formatter declared as above the date gets converted to 2017-10-28 18:30:00 +0000 which then can be easily parsed and accessed.

Notice how date is formatted and set to previous date. This is because 10/29/2017 is read by iOS as midnight time. Since I am following Indian time and date is formatted with respect to UTC (UTC is 5 Hours 30 minutes behind IST), formatted date is shown as 5 Hours and 30 minutes behind the date received in the JSON

Handling incoming JSON Arrays with Decoder

The example we have seen only handles the JSON response with single top level object. But what if the JSON is an array of dictionaries which needs to be converted into array of native models?

To begin with, let's assume incoming JSON with collection of array looks like this,

let jsonData = """
                [
                {"first": "Tom", "last": "Smith", "age": 31},
                {"first": "Bob", "last": "Smith", "age": 28}
                ]
               """

Now, we will convert this JSON into collection of object of type Person


// MARK: Struct Person 
Struct Person {
    let first: String
    let last: String
    let age: Int
}

// Variable persons is an array with objects of type Person after decoding operation.
let persons = try! jsonDecoder.decode([Person].self, from: jsonData.data(using: .utf8)!)

Please notice how we have replaced Type.self in decode method with [Type].self. You can also replace [Type].self with Array<Type>.self. The choice of option depends on the style guide and preferences.

Handling incoming JSON Dictionary with Decoder

Similar to handling JSON arrays, Codable also allows to de-serialize dictionary in the incoming JSON. Let's say incoming JSON looks like follows,

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

Let's say we want to parse this JSON into Swift native array of dictionaries. Where key is a String and associated value is a variable of type Person

// decodedDictionary will contain the object model representation (String: Person) of above JSON modelled as dictionary of type [String: Dictionary]

if let decodedDictionary = try? jsonDecoder.decode([String: Person].self, from: jsonDictionary.data(using: .utf8)!) {
    print(decodedDictionary)
}

Extension

Above example assumes that there is 1-1 mapping between property name and field name in the incoming JSON. However, what happens when struct Person has property named age and incoming JSON has field called person_age. In this case you can use Coding Keys enum and add alternate name to the property as follows.

struct Person: Codable {
    let first: String
    let last: String
    let age: Int

    enum CodingKeys: String, CodingKey {
        case first
        case last
        case age = "person_age"
    }
}

Please note that if no such mapping is provided, decoding will fail and control will reach into catch block

I am thankful to several people and blogs which helped me make this tutorial. This should be enough as long as basic knowledge of Codable is concerned. I have tried to add intermediate examples as well. 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:

Jayesh Kawli

I am a web and mobile developer working at Wayfair in Boston, MA. I come to learn so many things during course of life and I write about things which helped me and feel like they can help others too.

Subscribe to Fresh Beginning

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!