Making Custom Serialization a Breeze in Swift 5.1


Codable is already the simplest way to deal with serialization in Swift. Lots of boiler plate or 3rd party libraries are no longer required just to encode your simple Types into JSON. For those of us who started doing iOS development with stone age tools *cough Objective-C \cough* it's been a breath of fresh air. Finally we have one of those features Java(Script) and .net developers brag about that we totally haven't been in denial about needing!

However it still has some rough edges. One that's given me some lacerations is when your encoding doesn't match the defaults. So why is CodableWrappers needed?

How it Currently Works

Say you have a book catalog. Simply adhering to Codable allows it to be (de)serialized with any Decoder/Encoder.

struct Catalog: Codable {
    // Milliseconds Since 1970
    let updatedAt: Date
    let books: [Book]
}
struct Book: Codable {
    let count: Int
    let title: String
    let author: String
    /// Seconds Since 1970
    let released: Date
}
let myCatalog = Catalog(updatedAt: Date(timeIntervalSince1970: 1568937600),
                        books: [
                            Book(count: 5,
                                 title: "The Hitchhiker's Guide to the Galaxy",
                                 author: "Douglas Adams",
                                 released: Date(timeIntervalSince1970: 308534400))
                        ])
let jsonData = try? JSONEncoder().encode(myCatalog)

Which serializes into...

{
    "updatedAt": 590630400,
    "books": [
        {
            "author": "Douglas Adams",
            "title": "The Hitchhiker's Guide to the Galaxy",
            "count": 5,
            "released": -669772800
        }
    ]
}

Easy! Except... Date encodes using timeIntervalSinceReferenceDate. Since the server is using Unix Time for Book.published it's receiving 1948 not 1979), and Catalog.updatedAt is supposed to use milli-seconds. JSONEncoder allows customizing the date encoding with dateEncodingStrategy but you can only use one format.

On top of that, what if you also want to serialize it into Property List XML? PropertyListEncoder can be used, but it doesn't even have a dateEncodingStrategy (or much of any other settings) soo the only way to keep using Codable is customizing the implementations.

So our nice tidy code turns into this monstrosity...

struct Catalog: Codable {
    // Milliseconds Since 1970
    let updatedAt: Date
    let books: [Book]

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.books = try values.decode([Book].self, forKey: .books)
        let updatedMilliseconds = values.decode([Double.self, forKey: .updatedAt)
        self.updatedAt = Date(timeIntervalSince1970: updatedMilliseconds / 1000)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(books, forKey: .books)
        try container.encode(updatedAt.timerIntervalSince1970 * 1000, forKey: .updatedAt)
    }
}

struct Book: Codable {
    let count: Int
    let title: String
    let author: String
    /// Seconds Since 1970
    let released: Date

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.count = try values.decode(Int.self, forKey: .count)
        self.title = try values.decode(Int.self, forKey: .title)
        self.author = try values.decode(Int.self, forKey: .author)
        let secondsSince = values.decode([Double.self, forKey: .released)
        self.released = Date(timeIntervalSince1970: secondsSince)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(count, forKey: .count)
        try container.encode(title, forKey: .title)
        try container.encode(author, forKey: .author)
        try container.encode(released.timerIntervalSince1970, forKey: .released)
    }
}

So much for avoiding boilerplate 😭


Enter Swift 5.1 and Property Wrappers

One of the coolest new features in Swift 5.1 are Property Wrappers Swift 5.1. If you're looking for an overview I like NSHipster's, but from the proposal it allows "a property declaration to state which wrapper is used to implement it." Some working examples were put in the Burritos library.

How does this help our Catalog problem? Well rather than customizing the JSONEncoder or customizing your Codable implementation, we can write Property Wrappers.

@propertyWrapper
struct SecondsSince1970DateCoding {
    let wrappedValue: Date

    init(from decoder: Decoder) throws {
        let timeSince1970 = try TimeInterval(from: decoder)
        wrappedValue = Date(timeIntervalSince1970: timeSince1970)
    }

    func encode(to encoder: Encoder) throws {
        return try wrappedValue.timeIntervalSince1970.encode(to: encoder)
    }
}

@propertyWrapper
struct MillisecondsSince1970DateCoding {
    let wrappedValue: Date

    init(from decoder: Decoder) throws {
        let timeSince1970 = try TimeInterval(from: decoder)
        wrappedValue = Date(timeIntervalSince1970: timeSince1970 / 1000)
    }

    func encode(to encoder: Encoder) throws {
        return try (wrappedValue.timeIntervalSince1970 * 1000).encode(to: encoder)
    }
}

And now to re-simplify our Models.

struct Book: Codable {
    @SecondsSince1970DateCoding
    var published: Date
    let uuid: String
    let title: String
    let author: String
}

struct Catalog: Codable {
    @MillisecondsSince1970DateCoding
    var updatedAt: Date
    let books: [Book]
}

let catalogJSON = try? JSONEncoder().encode(myCatalog)

And... that's it! No boilerplate, no (En/De)coder options, just the models. Since it's customizing the property's Codable implementation it works for all Encoders and Decoders! It's even self documenting since the expected serialization strategy is right before the property!

CodableWrappers

With this in mind, I put together a handy little library that comes with all the value serialization options available in JSONEncoder and JSONDecoder out of the box: https://github.com/GottaGetSwifty/CodableWrappers.

It's also designed to make writing your own as easy as possible. For example if you need Nanoseconds since 1970.

struct NanosecondsSince9170Coder: StaticCoder {

    static func decode(from decoder: Decoder) throws -> Date {
        let nanoSeconds = try Double(from: decoder)
        let seconds = nanoSeconds * 0.000000001
        return Date(secondsSince1970: seconds)
    }

    static func encode(value: Date, to encoder: Encoder) throws {
        let nanoSeconds = value.secondsSince1970 / 0.000000001
        return try nanoSeconds.encode(to: encoder)
    }
}

// Approach 1: CodingUses
struct MyType: Codable {
    @CodingUses<NanosecondsSince9170Coder>
    var myData: Date // Now uses the NanosecondsSince9170Coder for serialization
}

// Approach 2: CodingUses propertyWrapper typealias

typealias NanosecondsSince9170Coding = CodingUses<NanosecondsSince9170Coder>

struct MyType: Codable {
    @NanosecondsSince9170Coding
    var myData: Date // Now uses the NanosecondsSince9170Coder for serialization
}

There are more examples on github, so head over there if you're interested!

If you've got a fever and the only cure is more CodableWrappers, head over to the more technical breakdown

Previous
Previous

Design Footnotes for CodableWrappers