Mario's tech stuff

Swift Codable: tips and tricks

Lets dive into Codable to understand the benefits of migrating from your third-party json library.

What is Codable?

If you take a look into theCodabledefinition youll find out that its just a way to tell thatsomethingisDecodableandEncodable.

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

Making your custom types conform these protocols will allow you to represent them in different formats such as JSON or property list (Plist).

Why is it a game changer?

Today every App is connected with web services and most of them provide data using the JSON format for its ease of reading and generating for both humans and machines.

In order to manage JSON data, the client needs to translate it in objects that can be easily manipulated (eg: structs or classes); this process is called parsing and the part of the app that does the job is the parser.

By implementing the Codable protocol in your data model you can forget about parses because your objects will be automatically translated from/to JSONs.

Simple usage

Suppose we want to know some basic information about this post and the API give us something like this:

{
  "userId": 12212,
  "id": 112371239,
  "title": "Swift 4 Codable: tips and tricks",
  "subtitle": "Many of you may agree with me when I say the Codable protocol is the best Swift 4 feature, here are some of my impressions about it."
}

The only thing to do now is make our Post struct conforms Decodable protocol and use aJSONDecoder.

import Foundation

let postInfoJson = """
{
    "userId": 12212,
    "id": 112371239,
    "title": "Swift 4 Codable: tips and tricks",
    "subtitle": "Many of you may agree with me when I say the Codable protocol is the best Swift 4 feature, here are some of my impressions about it."
}
"""

struct Post: Decodable {
    var id: Int?
    var userId: Int?
    var title: String?
    var subtitle: String?
}

let decoder = JSONDecoder()

if let jsonData = postInfoJson.data(using: .utf8) {
    do {
        let post = try decoder.decode(Post.self, from: jsonData)
        print(post)
    } catch let error as NSError {
        print(error)
    }
}

Thats pretty straightforward right?

Whats happening is that the decoder is mapping the jsons fields with the Post objects properties that have the exaclty same names.

In order to map objects properties with jsons field that have different names we need to tell the decoder where to gosearching. We can do that with a simple enum named CodingKeys that conforms CodingKey where the raw values of the cases match the json properties names.

let postInfoJson = """
{
"userId": 12212,
"id": 112371239,
"__title": "Swift 4 Codable: tips and tricks",
"__subtitle": "Many of you may agree with me when I say the Codable protocol is the best Swift 4 feature, here are some of my impressions about it."
}
"""
struct Post: Decodable {
    var id: Int?
    var userId: Int?
    var title: String?
    var subtitle: String?
    
    enum CodingKeys: CodingKey, String {
        case id
        case userId
        case title = "__title"
        case subtitle = "__subtitle"
    }
}

let decoder = JSONDecoder()

if let jsonData = postInfoJson.data(using: .utf8) {
    do {
        let post = try decoder.decode(Post.self, from: jsonData)
        print(post)
    } catch let error as NSError {
        print(error)
    }
}

One important thing to notice is that the name of the enummustbe CodingKeys, if we want to change is we need to provide a custom init.

let postInfoJson = """
{
"userId": 12212,
"id": 112371239,
"__title": "Swift 4 Codable: tips and tricks",
"__subtitle": "Many of you may agree with me when I say the Codable protocol is the best Swift 4 feature, here are some of my impressions about it."
}
"""
struct Post: Decodable {
    var id: Int?
    var userId: Int?
    var title: String?
    var subtitle: String?
    
    enum CustomJsonKeys: String, CodingKey {
        case id
        case userId
        case title = "__title"
        case subtitle = "__subtitle"
    }
    
    init(from decoder: Decoder) throws {
        let decoderContainer = try decoder.container(keyedBy: CustomJsonKeys.self)
        
        self.id = try decoderContainer.decode(Int.self, forKey: .id)
        self.userId = try decoderContainer.decode(Int.self, forKey: .userId)
        self.title = try decoderContainer.decode(String.self, forKey: .title)
        self.subtitle = try decoderContainer.decode(String.self, forKey: .subtitle)
    }
}

let decoder = JSONDecoder()

if let jsonData = postInfoJson.data(using: .utf8) {
    do {
        let post = try decoder.decode(Post.self, from: jsonData)
        print(post)
    } catch let error as NSError {
        print(error)
    }
}

Beside CusomJsonKeys enum that is pretty self-explanatory one thing to notice is that in this case we need to add a new function: init(from decoder: Decorer)throw.

In this method we provide the decoder with our custom keys in order to get a container that is an instance of KeyedDecodingContainer. You can think the container as some kind of dictionary.

We can agree that the syntax is a little bit messy and not very readable but Ive done this extension to make things easier.

extension KeyedDecodingContainer {
    subscript(key: KeyedDecodingContainer.Key) -> T? {
        return try? decode(T.self, forKey: key)
    }
}

By adding this extension you will be able to use the KeyedDecodingContainer just as you would do with a dictionary.

init(from decoder: Decoder) throws {
    let decoderContainer = try decoder.container(keyedBy: CustomJsonKeys.self)
    
    self.id = decoderContainer[.id]
    self.userId = decoderContainer[.userId]
    self.title = decoderContainer[.title]
    self.subtitle =  decoderContainer[.subtitle]
}

Nested objects

Nothing changes with nested object, the only think you need to remember is that even the nested objects need to conform the Decodable protocol and need to provide a custom init where needed.

Flatten nested objects

When we have a nested JSON but we want to flatten it into one single object we could use the functionnestedContainer(keyedBy: forKey:)

let postInfoJson = """
{
    "userId": 12212,
    "id": 112371239,
    "title": "Swift 4 Codable: tips and tricks",
    "subtitle": "Many of you may agree with me when I say the Codable protocol is the best Swift 4 feature, here are some of my impressions about it.",
    "counters": {
        "views": 100,
        "applause": 10
    }
}
"""
struct Post: Decodable {
    var id: Int?
    var userId: Int?
    var title: String?
    var subtitle: String?
    var views: Int?
    var applause: Int?
    
    enum CodingKeys: String, CodingKey {
        case userId
        case id
        case title
        case subtitle
        case counters
    }
    
    enum CountersCodingKeys: String, CodingKey {
        case views
        case applause
    }
    
    init(from decoder: Decoder) throws {
        let decoderContainer = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try decoderContainer.decode(Int.self, forKey: .id)
        self.userId = try decoderContainer.decode(Int.self, forKey: .userId)
        self.title = try decoderContainer.decode(String.self, forKey: .title)
        self.subtitle = try decoderContainer.decode(String.self, forKey: .subtitle)
        let nestedContainer = try decoderContainer.nestedContainer(keyedBy: CountersCodingKeys.self, forKey: .counters)
        self.views = try nestedContainer.decode(Int.self, forKey: .views)
        self.applause = try nestedContainer.decode(Int.self, forKey: .applause)
    }
}

let decoder = JSONDecoder()

if let jsonData = postInfoJson.data(using: .utf8) {
    do {
        let post = try decoder.decode(Post.self, from: jsonData)
        print(post)
    } catch let error as NSError {
        print(error)
    }
}

Dynamic keys

Sometimes we dont want to have fixed values for our coding keys but we want to be able to build them at run time. This is possible using a concrete implementation of the protocol CodingKey. Let’s call it ConcreteCodingKeys.

Using ConcreteCodingKeys it is possible to use any runtime string as coding key like that: something = try container.decode(String.self, forKey: GenericCodingKeys(stringLiteral: "aDynamicKey\(3+4)")

This technique is useful when, for example, you want to flatten multiple properties into a single array like in the following example.

import Foundation

let json = """
{
    "aProperty1": "Property 1",
    "aProperty2": "Property 2",
    "aProperty3": "Property 3",
    "aProperty4": "Property 4"
}
"""

struct ConcreteCodingKey: CodingKey {
    
    // MARK: CodingKey
    
    var stringValue: String
    var intValue: Int?
    
    init?(stringValue: String) {
        self.stringValue = stringValue
    }
    init?(intValue: Int) {
        return nil
    }
    
    init(string: String) {
        self.stringValue = string
    }
}

struct SimpleStruct: Decodable {
    var flattenProperties: [String]?
    
    init(from decoder: Decoder) throws {
        flattenProperties = [String]()
        let decoderContainer = try decoder.container(keyedBy: ConcreteCodingKey.self)
        var index = 1
        while let property = try? decoderContainer.decode(String.self, forKey: ConcreteCodingKey(string: "aProperty\(index)")) {
            flattenProperties?.append(property)
            index += 1
        }
    }
}

let decoder = JSONDecoder()

if let jsonData = json.data(using: .utf8) {
    do {
        let post = try decoder.decode(SimpleStruct.self, from: jsonData)
        print(post)
    } catch let error as NSError {
        print(error)
    }
}

To infinity and beyond

Conclusions

Using Codable instead of a third-party library (such as Marshal or SwiftyJSON) could look tricky at the beginning but after a some pratice youll able to achieve anything you want with (usually) less code and since youre using a part of the standard SDK your app will also be smaller in terms of size.