Weekly Swift articles, podcasts and tips by John Sundell.

Customizing Codable types in Swift

Published on 07 Jul 2019
Basics article available: Codable

One thing that most modern applications have in common is that they need to encode or decode various forms of data. Whether that’s JSON data that was downloaded over the network, or some form of serialized representation of a model that’s stored locally — being able to reliably encode and decode different pieces of data is essential for more or less any Swift code base.

That’s a big part of why Swift’s Codable API was such a significant new feature when it was introduced as part of Swift 4.0 — and since then it has grown to become a standard, robust mechanism for several different kinds of encoding and decoding — both on Apple’s platforms, as well as for server-side Swift.

What makes Codable so great is that it’s tightly integrated into the Swift toolchain, making it possible for the compiler to auto-synthesize a lot of the code needed to both encode and decode various values. However, sometimes we do need to customize how our values are represented when serialized — so this week, let’s take a look at a few different ways that we can tweak our Codable implementations to do just that.

Changing keys

Let’s start with one of the basic ways that we can customize the way a type is encoded and decoded — by modifying the keys that are used as part of its serialized representation. Let’s say that we’re working on an app for reading articles, and that one of our core data models looks like this:

struct Article: Codable {
    var url: URL
    var title: String
    var body: String
}

Our model currently uses a completely auto-sythesized Codable implementation, meaning that all of its serialization keys will match the names of its properties. However, the data that we’ll decode Article values from — for example JSON downloaded from a server — might use a slightly different naming convention, causing that default decoding to fail.

Thankfully, that’s easily fixed. All we have to do to customize what keys that Codable will use when decoding (or encoding) instances of our Article type is to define a CodingKeys enum within it — and assign custom raw values to the cases matching the keys that we wish to customize — like this:

extension Article {
    enum CodingKeys: String, CodingKey {
        case url = "source_link"
        case title = "content_name"
        case body
    }
}

Doing the above lets us keep leveraging the compiler-generated default implementation for the actual coding work, while still enabling us to change the names of the keys that’ll be used for serialization.

While the above technique is great for when we want to use completely custom key names, if we only wanted Codable to use snake_case versions of our property names (for example turning backgroundColor into background_color) — then we could’ve simply changed our JSON decoder’s keyDecodingStrategy instead:

var decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

What’s great about the above two APIs is that they enable us to work around mismatches between our Swift models and the data that’ll be used to represent them, without requiring us to modify the names of our properties.

Ignoring keys

While it’s really useful to be able to customize the names of coding keys, sometimes we might want to completely ignore certain ones. For example, let’s now say that we’re working on a note taking app — and that we enable the user to group various notes together to form a NoteCollection, which can include local drafts:

struct NoteCollection: Codable {
    var name: String
    var notes: [Note]
    var localDrafts = [Note]()
}

However, while it’s really convenient to have localDrafts be a part of our NoteCollection model — let’s say that we don’t want those drafts to be included when serializing or deserializing such a collection. A reason for that might be to give the user a clean slate every time they launch the app, or because our server doesn’t support drafts.

Luckily, that can also easily be done without having to change the actual Codable implementation for NoteCollection. If we define a CodingKeys enum, just like before, and simply omit localDrafts — then that property won’t be taken into account when either encoding or decoding a NoteCollection value:

extension NoteCollection {
    enum CodingKeys: CodingKey {
        case name
        case notes
    }
}

For the above to work, the property that we’re omitting must have a default value — which localDrafts already had in this case.

Creating matching structures

So far we’ve only been tweaking a type’s coding keys — and while we often can get quite far by just doing that, sometimes we need to go a bit further in terms of our Codable customization.

Let’s say that we’re building an app that includes a currency conversion feature — and that we’re downloading the current exchange rates for a given currency as JSON data, which looks like this:

{
	"currency": "PLN",
	"rates": {
		"USD": 3.76,
		"EUR": 4.24,
		"SEK": 0.41
	}
}

Within our Swift code, we then want to convert such JSON responses into CurrencyConversion instances — which each includes an array of ExchangeRate entries — one for each currency:

struct CurrencyConversion {
    var currency: Currency
    var exchangeRates: [ExchangeRate]
}

struct ExchangeRate {
    let currency: Currency
    let rate: Double
}

However, if we just went ahead and made both of the above two models conform to Codable, we’d again end up with a mismatch between our Swift code and the JSON data that we’re looking to decode. But this time, it’s not just a matter of key names — there’s a fundamental difference in structure.

We could, of course, modify the structure of our Swift models to exactly match the structure of our JSON data — but that’s not always practical. While having correct serialization code is important, having a model structure that fits our actual code base is arguably just as important.

Instead, let’s create a new, dedicated type — that’ll act as a bridge between the format used within our JSON data, and the structure of our Swift code. Within that type we’ll be able to encapsulate all of the logic needed to turn a JSON dictionary of exchange rates into an array of ExchangeRate models — like this:

private extension ExchangeRate {
    struct List: Decodable {
        let values: [ExchangeRate]

        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let dictionary = try container.decode([String : Double].self)

            values = dictionary.map { key, value in
                ExchangeRate(currency: Currency(key), rate: value)
            }
        }
    }
}

Using the above type, we can now define a private property, which name matches the JSON key used for its data — and have our exchangeRates property simply act as a public-facing proxy for that private property:

struct CurrencyConversion: Decodable {
    var currency: Currency
    var exchangeRates: [ExchangeRate] {
        return rates.values
    }
    
    private var rates: ExchangeRate.List
}

The reason the above works is because computed properties are never taken into account when encoding or decoding a value.

The above technique can be a great tool when we want to make our Swift code compatible with a JSON API that uses a very different structure — again without having to implement Codable completely from scratch.

Transforming values

A very common issue when it comes to decoding, especially when working with external JSON APIs that are beyond our control, is when types are encoded in a way that’s incompatible with Swift’s strict type system. For example, the JSON data that we’re looking to decode might use strings to represent integers or other kinds of numbers.

Let’s take a look at a way that can let us deal with such values, again in a self-contained way that doesn’t require us to write a completely custom Codable implementation.

What we’re essentially looking to do here is to transform string values into another type — let’s take Int as an example. We’ll start by defining a protocol that’ll let us mark any type as being StringRepresentable — meaning that it can both be converted from and into a string representation:

protocol StringRepresentable: CustomStringConvertible {
    init?(_ string: String)
}

extension Int: StringRepresentable {}

We’re basing our above protocol on CustomStringConvertible from the standard library, since that already includes a property requirement for describing a value as a string. For more on this way of defining protocols as specializations of other ones, check out “Specializing protocols in Swift”.

Next, let’s create another dedicated type — this time for any value that can be backed by a string — and have it contain all of the code needed to decode and encode a value to and from a string:

struct StringBacked<Value: StringRepresentable>: Codable {
    var value: Value
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let string = try container.decode(String.self)
        
        guard let value = Value(string) else {
            throw DecodingError.dataCorruptedError(
                in: container,
                debugDescription: """
                Failed to convert an instance of \(Value.self) from "\(string)"
                """
            )
        }
        
        self.value = value
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value.description)
    }
}

Just like how we previously created a private property for our JSON-compatible, underlying storage, we can now do the same for any property that’s backend by a string when encoded — while still exposing that data to the rest of our Swift code as its proper type. Here’s an example of doing just that for a Video type’s numberOfLikes property:

struct Video: Codable {
    var title: String
    var description: String
    var url: URL
    var thumbnailImageURL: URL
    
    var numberOfLikes: Int {
        get { return likes.value }
        set { likes.value = newValue }
    }
    
    private var likes: StringBacked<Int>
}

There’s definitely a tradeoff here between the complexity of having to manually define setters and getters for a property, and the complexity of having to fall back to a completely custom Codable implementation — but for types like the above Video struct, which only has one property in need of customization, using a private backing property can be a great option.

Conclusion

While it’s truly fantastic that the compiler is capable of automatically synthesizing all Codable conformances that don’t require any form of customization — the fact that we’re able to customize things when needed is equally fantastic.

Even better is that doing so often doesn’t actually require us to completely abandon the auto-generated code in favor of a manual implementation — it’s many times possible to just slightly tweak the way a type is encoded or decoded, while still letting the compiler do most of the heavy lifting.

What do you think? Do you have any favorite ways of customizing the way Codable works in Swift, and will some of the above techniques be useful when working with Codable in your project? Let me know, either via email or Twitter.

Thanks for reading! 🚀