Episode #424

Codable Property Wrappers

8 minutes
Published on January 17, 2020

This video is only available to subscribers. Get access to this video and 574 others.

When working with Codable models, everything is great until you deviate even slightly from the normal behavior. Doing so requires you to drop down to a more manual approach where you have to define your own CodingKeys enum and encode/decode methods. In this episode we will look at a 3rd party set of property wrappers that give you some handy Codable functionality for free.

Episode Links

When working with Codable models, everything is great until you deviate even slightly from the normal behavior.

Doing so requires you to drop down to a more manual approach where you have to define your own CodingKeys enum and encode/decode methods.

This offers a lot of flexibility, but it can mean all of this complexity is introduced the instant you deviate from the way Codable works out of the box.

In this episode we’ll take a look at BetterCodable, which is a 3rd party set of property wrappers that simplify some Codable issues that would normally require you write your own encode/decode methods.

Take this example for instance. Here we have some JSON that represents some users:

{
    "users": [
        {
            "firstName": "Jim",
            "lastName": "Halpert",
            "age": 34,
            "birthday": "1982-05-24",
            "createdAt" : "2020-01-14T15:14:43+0000"
        },
        {
            "firstName": "Pam",
            "lastName": "Beesly",
            "age": 29,
            "birthday": "1987-10-12",
            "createdAt" : "2020-01-14T15:14:43+0000"
        },
    ]
}

Parsing this is easy with Codable:

struct User : Codable {
    let firstName: String
    let lastName: String
    let age: Int
}

We also need a wrapper object to decode the users array:

struct UsersWrapper : Codable {
    let users:  [User]
}

Here we’re just parsing a few of the properties from the JOSN, but it works without any further work. We can use JSONDecoder as is and get back our users:

let data = usersJSON.data(using: .utf8)!
let decoder = JSONDecoder()
let users = try! decoder.decode(UsersWrapper.self, from: data).users

print(users)

If we wanted to also include the createdAt attribute, we add the property to our User:

    let createdAt: Date

And then set the dateDecodingStrategy of our decoder to use ISO 8601 Date formatting:

decoder.dateDecodingStrategy = .iso8601

So far so good. But what about birthdate? We can’t parse that as a date because it doesn’t share the same format.

This is one of those cases where you’d have to drop down and implement the init(from: Decoder) methods yourself.

Integrating BetterCodable

BetterCodable is a Swift package from Mark Sands that adds some really useful property wrappers when dealing with Codable models.

After downloading the Swift package, we'll import it at the top:

import BetterCodable

Then we can decorate our date properties with a @DateValue property wrapper. This is a generic type that accepts a DateStrategy for how to decode this particular value.

    @DateValue<ISO8601Strategy>
    var createdAt: Date

    @DateValue<YearMonthDayStrategy>
    var birthday: Date

Now we can specify which strategy we want on a per-attribute level instead of at the decoder. This means we can also omit the dateDecodingStrategy, since it is no longer needed.

Lossless Values

If any part of our JSON document has the wrong type, the entire thing fails to decode. This is good for safety, but it would be nice to be more resilient to these issues and parse data that is close enough to the format we want.

For instance, what if one of the records had age as a String instead of an Int? If the value could be parsed to an Int then we wouldn't have to fail. This is where the @LosslessValue wrapper comes in.

    @LosslessValue
    var age: Int

Now if we get a "29" it will be converted to an integer 29. Neat!

Lossy Collections

Similarly, if any of the users fail to decode because one of them is missing data, then the entire thing will fail. Sometimes it is useful to skip these bad records and decode the rest.

Doing so would normally require a custom decode implementation, but with the @LossyArray property wrapper we'll get this behavior for free.

@LossyArray
var users: [User]

There is also a @LossyDictionary that works for dictionaries.

This episode uses Xcode 11.3, Swift 5.1.