Codable magic

Feb 15, 2018 · Follow on Twitter and Mastodon swiftcodable

I have finally started replacing all NSCoding objects in my code with the new Codable protocol. This article covers things that I’ve learned along the way.

The models in this article are fictional representations of a real world domain model. If they seem too simple or if there are bugs, it’s because the code was only written for this post.

Model

Lets say that we have Movie and MovieGenre structs that look like this:

struct Movie {
    
    let id: Int
    let name: String
    let releaseDate: Date
    let genre: MovieGenre
}

enum MovieGenre: String {
    
    case action
    case drama
    ...
}

We may want to use these structs in various ways, e.g. serializing and deserializing them. Let’s compare how this is traditionally done with NSCoding and how Codable can simplify it.

NSCoding

With NSCoding, you just let the type you want to serialize implement the NSCoding protocol. However, this requires the type to be a class and not a struct:

import Foundation

class Movie: NSObject, NSCoding {
    
    // MARK: - Initialization

    // ...more initializers
    
    required init?(coder: NSCoder) {
        guard
            let id = coder.decodeObject(forKey: idKey) as? Int,
            let name = coder.decodeObject(forKey: nameKey) as? String,
            let date = coder.decodeObject(forKey: releaseDateKey) as? Date,
            let _genre = coder.decodeObject(forKey: genreKey) as? String,
            let genre = MovieGenre(rawValue: _genre)
            else { return nil }
        self.id = id
        self.name = name
        self.releaseDate = date
        self.genre = genre
    }
    
    // MARK: - Mapping Keys
    
    private let idKey = "id"
    private let nameKey = "name"
    private let releaseDateKey = "releaseDate"
    private let genreKey = "genre"

    // MARK: - Properties
    
    let id: Int
    let name: String
    let releaseDate: Date
    let genre: MovieGenre
    
    // MARK: - Public Functions
    
    func encode(with coder: NSCoder) {
        coder.encode(NSNumber(value: id), forKey: idKey)
        coder.encode(name, forKey: nameKey)
        coder.encode(releaseDate, forKey: releaseDateKey)
        coder.encode(genre.rawValue, forKey: genreKey)
    }
}

In the code above, we inherit NSObject and implement our codable logic, which involves a lot of code, strings, etc., This has to be repeated for every codable type, which is tedious and error-prone.

Let’s take a look at how Codable can make this a lot leaner and safer.

Codable

With Codable, you just let the type you want to serialize implement the Codable protocol instead. Codable doesn’t require you to use classes, so our types can keep being structs:

import Foundation

struct Movie: Codable {

    let id: Int
    let name: String
    let releaseDate: Date
    let genre: MovieGenre
}

With the code above, however, the compiler will complain that Movie doesn’t implement Decodable.

If you’re new Codable, you would perhaps throw code at the problem to make the Movie codable:

import Foundation

struct Movie: Codable {

    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        name = try values.decodeIfPresent(String.self, forKey: .name)
        releaseDate = try values.decode(Date.self, forKey: .releaseDate)
        genre = try values.decode(BookFormat.self, forKey: .genre)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try container.encode(releaseDate, forKey: .releaseDate)
        try container.encode(genre, forKey: .genre)
    }

    private enum CodingKeys: String, CodingKey { case
        id,
        name,
        releaseDate,
        genre
    }
    
    var id: Int
    var name: String
    var releaseDate: Date
    var genre: MovieGenre
}

This is however not needed! If you look at the error message, the only reason why Movie isn’t correctly implementing the protocol is because MovieGenre isn’t.

All we have to do to fix the error is to make MovieGenre implement Codable too, which in this case is easily done by just adding Codable to the type:

enum MovieGenre: String, Codable { 
    
    case action
    case drama
    ...
}

This is a simple model, but even if your models contain a lot more stuff, and maybe property types that don’t conform to Codable, this is basically how you do it.

If your model contains non-codable types, you can either change the model or implement the Codable protocol for your non-codable types. For instance, a Color can extract its RGBA data.

Happy encoding!

Discussions & More

Please share any ideas, feedback or comments you may have in the Disqus section below, or by replying on Twitter or Mastodon..

If you found this text interesting, make sure to follow me on Twitter and Mastodon for more content like this, and to be notified when new content is published.

If you like & want to support my work, please consider sponsoring me on GitHub Sponsors.