PJ Fechner PJ Fechner

Design Footnotes for CodableWrappers

More technical details that didn’t fit in the main post.


Not looking for so many nerdy details or don't know what this is all about? Head over here for a more general breakdown of the library and the problems it solves.

Introduction

Custom Serialization with Codable is currently either up to the (en/de)coder being used, or requires custom implementations.

When Property Wrappers was announced I realized, (after some messing around and several rabbit trails), they can customize the Codable implementation for an individual property.

Advantages of Property Wrappers

Replacing the current approach with a Property Wrapper gets a lot of advantages from being implemented at the Property level, rather than the (en/de)coder or Type level:

  • Declare once for all Encoders and Decoders. (JSONEncoder, PropertyListDecoder, or any 3rd party implementations)
  • Custom (de/en)coding without overriding encode(to: Encoder) or init(with decoder) for your whole Type to customize one property
  • Multiple (de/en)coding strategies for different properties within the same Type and/or child Types

Using an Attribute also results in an improved Design:

  • It's declarative at the point of use. No custom implementation code, no options to set on your (en/de)coders
  • It's self-documenting. The serialization is defined by the property, so comments are no longer needed to say as much and there's no de-synchronization between documented serialization and (en/de)coders
  • It can be reused in any Type without writing any additional code.

I will be using this approach whenever viable, so a library that simplifies this process seemed like a good idea!

However there are some strict limitations of Codable and Property Wrappers. Finding ways around these road blocks was an exercise in iteration and creativity.

Codable Property Wrapper Constraints

Codable and Property Wrappers are focussed on solving specific problems in specific ways. This is a good approach, and can also limit what's possible.

Codable Constraints

Since Decodable uses init for it's decoding, any customization must be accessible in a static context. This means customized decoding with Property Wrappers requires either separate implementations for each wrapper (no thank you), or a way to define that customization at the declaration point. This leaves these constraints:

  • Custom Decoding must be static
  • That customization needs to be injected at the declaration point

Property Wrapper Constraints

Since a Property Wrapper defines whether it's wrapped Property is mutable, both mutable and immutable versions are needed. To match Codable's API, separate Encodable and Decodable versions should be available. Filling these 2 constraints require 6 different Property Wrappers for each customization.

  • Encodable
  • Decodable
  • Codable
  • EncodableMutable
  • DecodableMutable
  • CodableMutable

Given the overhead required for many wrappers, the design should abstract away as much of the implementation as possible.

Final Design Constraints

  1. The decoding must be done statically
  2. That capability must be accessible within the Property Wrapper
  3. The Codable implementation can't rely on a separate Property Wrapper implementations
  4. It must easily support the 6 different wrapper types

Working Through Some Things

The first constraint can be solved by writing static versions of (En/De)codable.

*Note* the following implementations are limited to Codable for the sake of brevity. The actual implementations also include separate Encoder and Decoder variants

// Protocol
public protocol StaticCoder: Codable {
    associatedtype CodingType: Encodable
    /// Mirror of `Encodable`'s `encode(to: Encoder)` but in a static context
    static func encode(value: CodingType, to encoder: Encoder) throws
    /// Mirror of `Decodable`'s `init(from: Decoder)` but in a static context
    static func decode(from decoder: Decoder) throws -> CodingType
}

// Example Implementation
public struct Base64DataCoder: StaticCoder {
    private init() { }

    public static func decode(from decoder: Decoder) throws -> Data {
        let stringValue = try String(from: decoder)

        guard let value = Data.init(base64Encoded: stringValue) else {
            // throw an error
        }
        return value
    }

    public static func encode(value: Data, to encoder: Encoder) throws {
        try value.base64EncodedString().encode(to: encoder)
    }
}

Generics are a good candidate for #2 and #3. They allow a StaticCoder to be passed in at the declaration point.

// Wrapper
@propertyWrapper
public struct CodingUses<CustomCoder: StaticCoder>: Codable {

    public let wrappedValue: CustomCoder.CodingType
    public init(wrappedValue: CustomCoder.CodingType) {
        self.wrappedValue = wrappedValue
    }

    public init(from decoder: Decoder) throws {
        self.init(wrappedValue: try CustomCoder.decode(from: decoder))
    }

    public func encode(to encoder: Encoder) throws {
        try CustomCoder.encode(value: wrappedValue, to: encoder)
    }
}

// Usage
struct MyType: Codable {
    @CustomCoding<Base64DataCoder>
    var myBase64Data: Data
}

For #4, the Codable implementation can be put into protocols with default implementations.

public protocol StaticCodingWrapper: Codable {
    associatedtype CustomCoder: StaticCoder
}

extension StaticCodingWrapper {
    public init(from decoder: Decoder) throws {
        self.init(wrappedValue: try CustomCoder.decode(from: decoder))
    }

    public func encode(to encoder: Encoder) throws {
        try CustomCoder.encode(value: wrappedValue, to: encoder)
    }
}

And...Voilà!

The Property Wrapper can now be cut down to a manageable size enabling the simplest implementation possible for each of the 6 required versions with very little copy-pasting.

@propertyWrapper
public struct CodingUses<CustomCoder: StaticCoder>: StaticCodingWrapper {

    public let wrappedValue: CustomCoder.CodingType
    public init(wrappedValue: CustomCoder.CodingType) {
        self.wrappedValue = wrappedValue
    }
}

@propertyWrapper
public struct CodingUsesMutable<CustomCoder: StaticCoder>: StaticCodingWrapper {

    public var wrappedValue: CustomCoder.CodingType
    public init(wrappedValue: CustomCoder.CodingType) {
        self.wrappedValue = wrappedValue
    }
}

Adding More Wrappers

An (admittedly unintended 😁) side effect of this approach also means a new Property Wrapper can be added with a typealias so rather than dozens of implementations each new Wrapper can be written in one line!

// Wrappers

/// Encode this immutable `Data` Property as a Base64 encoded String
typealias Base64Encoding = EncodingUses<Base64DataStaticCoder>
/// Decode this immutable `Data` Property as a Base64 encoded String
typealias Base64Decoding = DecodingUses<Base64DataStaticCoder>
/// (En/De)code this immutable `Data` Property as a Base64 encoded String
typealias Base64Coding = CodingUses<Base64DataStaticCoder>

/// Encode this immutable `Data` Property as a Base64 encoded String
typealias Base64EncodingMutable = EncodingUsesMutable<Base64DataStaticCoder>
/// Decode this immutable `Data` Property as a Base64 encoded String
typealias Base64DecodingMutable = DecodingUsesMutable<Base64DataStaticCoder>
/// (En/De)code this immutable `Data` Property as a Base64 encoded String
typealias Base64CodingMutable = CodingUsesMutable<Base64DataStaticCoder>

// Usage
struct MyType: Codable {
    @Base64Coding
    var myBase64Data: Data
}

Custom Wrappers

Although a "full" implementation requires all 6 wrappers from a library point of view, the structure is easily extendable and most custom versions would only need one. So the simplest version of a custom coding is quite short.

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)
    }
}

typealias NanosecondsSince9170Coding = CodingUses<NanosecondsSince9170Coder>

This can be used as...

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

Wrapping Up

Swift is still quite young as programming languages go and is just now stabilizing. It's already enabled/popularized new design patterns for Apple developers and, in my experience, greatly reduced many common bugs and crashes compared to it's predecessors. There are (of course) still plenty of pain points, but the big growing pains are seemingly behind us and features like Property Wrappers being added makes me optimistic about the future of the language.

Read More
PJ Fechner PJ Fechner

Making Custom Serialization a Breeze in Swift 5.1

Codable is already the simplest way to deal with serialization in Swift but still has some rough edges around non-default encoding. That's where CodableWrappers comes in! Check out how this little library can delete a lot of code.


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

Read More