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)
orinit(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
- The decoding must be done statically
- That capability must be accessible within the Property Wrapper
- The Codable implementation can't rely on a separate Property Wrapper implementations
- 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.
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.
- @Base64Coding
- @SecondsSince1970DateCoding
- @MillisecondsSince1970DateCoding
- @ISO8601DateCoding
- @DateFormatterCoding
- @NonConformingFloatCoding
- @NonConformingDoubleCoding
- Other Customization
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