The Codable Protocol

Jeff Campbell
KPCC Labs
Published in
8 min readApr 5, 2019

One of the great joys of iOS development is watching the evolution of your tools as it streamlines the creative process.

If you’ve done iOS development for a while you’ve seen a fair number of these pop up over the years: Successively better versions of Xcode, the addition of auto-synthesizing properties, blocks, view controller containment, auto layout, ARC, size classes, etc.

The succession of more — and better — tools has made life for iOS developers much easier, allowing us to focus on making great things.

“Hammers and other tools arranged in a row” by Adam Sherez on Unsplash

The transition from Objective-C to Swift, too, has certainly borne fruit — though the sometimes steep learning curve and ever-evolving nature of the language can prove challenging at times.

(There are, of course, setbacks. The less said about the myriad ways iOS has handled device rotation over the years, the better!)

One of the most joy-inducing changes in not-too-distant history, for me, was the addition of the Codable protocol. Swift 5 was just released (and is garnering much acclaim), but — with the addition of Codable in Swift 4 — that was, to me, a much more exciting release.

The Codable Protocol

Codable is an exceedingly nifty Swift protocol.

In Swift (in contrast with Objective-C) there is a much stronger emphasis on composition over inheritance, with protocols used to add functionality to class, struct, or enum instances without the use of subclassing. A class, struct, or enum that implements a protocol is said to conform to it, gaining that protocol’s functionality in the process.

Codable is unique in that it is actually just shorthand for a union of both the Encodable and Decodable protocols. An instance can conform to either one independently, but if it conforms to both then it is also Codable conformant.

What do these protocols give us? Let’s take a look!

Codable Benefits

One common operation for many apps is the ingestation of content from an API. For example, our KPCC API Client downloads articles, episodes, and program schedule data and turns it into appropriate model instances for use in an app.

This is an example of decoding (also known as deserialization). We consume a serialized — in this case, JSON— representation of our data and turn it into something we can work with.

We can also do this in reverse, encoding (serializing) data into a format that can be either passed back up to a server or saved into the user’s device in the form of a local cache.

The long and short of it is that we get a standardized system for encoding/decoding and the opportunity to delete a lot of boilerplate code. Our KPCC API Client project leans heavily on the use of Codable to ingest API responses and use them to return native model instances.

Basic Encoding and Decoding

Conforming to Codable is simple. Assuming you have a struct, simply add the Codable protocol to your struct’s definition, like so:

struct Cat: Codable {
var age:Int
var name:String
}

That’s it! Now, if you wish to encode this Cat struct and save it to the user’s device and then read it back, you can do so like this:

let apollo       = Cat(age: 10, name: "Apollo")
let apolloData = try? JSONEncoder().encode(apollo)
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let fileURL = documentsURL?.appendingPathComponent("Cat.json")
FileManager.default.createFile(atPath: fileURL!.path, contents: apolloData!, attributes: nil)

(Note: For the sake of brevity I’ve left out a fair bit of error handling in this example. In a shipping app you’ll want to address this in an idiomatic Swift fashion.)

The above example will write your Cat instance to your app’s sandboxed Documents directory. If you were to open it up in a text editor, you would see something like the following JSON:

{
"age":9,
"name":"Apollo"
}

You can read the file back from the file system and decode it from JSON into a native Swift struct like this:

let apolloData = FileManager.default.contents(atPath: fileURL!.path)
if let apolloData = apolloData {
let apollo = try? JSONDecoder().decode(Cat.self, from: apolloData)
}

Handling Complexity

Apollo is a very complex cat, and as such he cannot be summed up in just a couple of properties. By the same token, your own classes and structs are likely to be comprised of various custom class, struct, and enum properties.

How is this handled in code? Simply conform those types to Codable as well!

Here’s an example of how one might define a multifaceted feline:

enum MeowFrequency: String, Codable {
case never
case occasional
case regular
case incessant
}
enum FurColor: String, Codable {
case orange
case tuxedo
case calico
case gray
case colorPoint
case tabby
case black
case white
}
enum FurLength: String, Codable {
case short
case medium
case long
}
struct Fur: Codable {
var color:FurColor
var length:FurLength
}
struct Cat: Codable {
var age:Int
var name:String
var meowFrequency:MeowFrequency
var fur:Fur
}

Note that — as shown in the example above — enums must have a defined raw value (in this case, String). This will be used to ascertain the encoded value.

When encoded to the device, our Cat struct instance’s JSON representation might look something like the following:

{
"age": 10,
"fur": {
"color": "black",
"length": "medium"
},
"name": "Apollo",
"meowFrequency": "incessant"
}

Custom Coding Keys

The above works pretty well when reading and writing instances to and from a device, but what about ingesting content from an API? The default keys used in encoding and decoding use the same naming convention as Swift properties (so-called “camelCase”) but many APIs use so-called “snake_case” as a convention instead.

We can override how keys are associated with properties by adding a special CodingKeys enum that maps one to the other.

For example, below we create the more suitably reptilian convention of meow_frequency for our meowFrequency property:

enum CodingKeys: String, CodingKey {
case age = "age"
case name = "name"
case fur = "fur"
case meowFrequency = "meow_frequency"
}

Note that you have to define a case for each stored property associated with your instance type, even if the key isn’t being rewritten.

One exception to this is if, for some reason, you want to omit a property from encoding/decoding altogether (because, for example, it is programmatically generated by some process in your code). Omitting it from the CodingKeys enum will do so.

Automatic Snake Case

With the advent of Swift 4.1, the ability to automatically convert properties to and from snake case was added. To do this, simply specify the appropriate keyEncodingStrategy and/or keyDecodingStrategy settings to your JSON encoder and decoder instances, like so:

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
// ...let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertToSnakeCase

You will still need to be careful how you name your properties — recognizing that hasHairball and horked_on_carpet refer to the same thing is a bit beyond these tools at the moment — but this change still provides an opportunity to get rid of boilerplate code.

Custom Decoding/Encoding

There are occasions where you might want to override the default encoding or decoding behavior in some fundamental way. This is quite common when ingesting 3rd party APIs, for instance, whose returned structure might not mirror your app’s model layer. Luckly, Codable allows you to manually construct your model based on what the API returns without having to mirror it exactly.

For example, let’s say that we are interacting with a CaaS (Cats as a Service) server API that returns individual cats on demand. Included in an API response is an array of toys that each cat likes to play with, in order of preference.

{
"age": 10,
"fur": {
"color": "black",
"length": "medium"
},
"name": "Apollo",
"meow_frequency": "incessant",
"favorite_toys": [
"something expensive and breakable",
"real mouse",
"toy mouse",
"scrunchy ball"
]
}

However, our app is only concerned with a cat’s favorite toy, so rather than keeping an array of toys in memory we just care about the first item. First, update our CodingKeys enum to add a new key:

enum CodingKeys: String, CodingKey {
case age = "age"
case name = "name"
case fur = "fur"
case meowFrequency = "meow_frequency"
case favoriteToy = "favorite_toys"
}

Then implement Decodable’s init(from:) method, like this:

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
age = try values.decode(Int.self, forKey: .age)
name = try values.decode(String.self, forKey: .name)
fur = try values.decode(Fur.self, forKey: .fur)
meowFrequency = try values.decode(MeowFrequency.self, forKey: .meowFrequency)
if let favoriteToys = try? values.decode([String].self, forKey: .favoriteToy) {
favoriteToy = favoriteToys.first
}
}

This will read from our API and set the favoriteToy property to the first — and most desirable — toy.

If your app allows users to create cats and send them to the cat server for posterity — a reasonable feature, you must agree — you’ll need to encode the resulting Cat instance into a format the server expects.

In this case, that means implementing Encodable’s encode(to:) method and using it to put favoriteToy into an array with a key that the server expects, “favorite_toys”:

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.age, forKey: .age)
try container.encode(self.name, forKey: .name)
try container.encode(self.meowFrequency, forKey: .meowFrequency)
var favoriteToys:[String] = []
if let favoriteToy = self.favoriteToy {
favoriteToys.append(favoriteToy)
}
try container.encode(favoriteToys, forKey: .favoriteToy)
}

As with CodingKeys, if you override the default decoding or encoding methods you must handle implementing behavior for all properties yourself — even those that don’t require special handling.

Containers

In the encoding example above, the encoder’s container(keyedBy:) method returns a KeyedEncodingContainer data structure to expose key/value pairs for encoding purposes. An equivalent KeyedDecodingContainer data structure is available when using the decoder’s equivalent container(keyedBy:) method as well. These are the most common containers you’ll use.

There are occasions, however, where you might need to encode and decode data structures contained within an array and lacking keys. You can do this using UnkeyedEncodingContainer and UnkeyedDecodingContainer and their associated methods.

Finally, on rare occasion you may want to encode or decode single values. You can do this using SingleValueDecodingContainer and SingleValueEncodingContainer and the methods they provide.

Considerations and Caveats

Here are a few matters of note when using Codable:

  • When specifying a collection type to decode, you must use array (ex. “[String]”) or dictionary (ex. “[String:String]”) notation where appropriate. The decoder needs to always know what kinds of instances to expect.
  • Properties defined as optionals do not need to be present when decoded. A missing value will simply set it to nil. If a property is not an optional, however, decoding will fail if the associated value isn’t present. You can specify custom behavior by implementing Decodable’s init(from:) method, if you wish.
  • It is common to work with Date instances in model instances, but JSON does not support native dates so it must somehow specify one using a more primitive data type. You can specify how this is done using JSONEncoder’s dateEncodingStrategy property and JSONDecoder’s dateDecodingStrategy property. Options you might consider are iso8601, secondsSince1970, millisecondsSince1970, or even a custom formatter of your own (with formatted(), specifying a customized DateFormatter instance).
  • Note that JSONDecoder’s ISO 8601 support is rather incomplete. For instance, the KPCC API returns dates in a variant of ISO 8601 that includes fractional seconds. This is unsupported prior to iOS 11, and we need to support at least as far back as iOS 10. To work around this, the KPCC API Client project uses DateFormatter with a custom defined dateFormat of “yyyy-MM-dd’T’HH:mm:ss.SSSXXXXX”. You can learn more about this workaround on StackOverflow.
  • It is rather difficult, in the existing implementation of Codable, to decode heterogenous arrays of disparate instances. If you control the underlying data format or API I would recommend avoiding heterogenous arrays entirely. If you wish to forge ahead and attempt to implement decoding for a heterogenous array, this article may be helpful.
  • While JSON is the most common format used with Codable, it can also encode/decode XML and Property List formats.
Apollo with Catnip

--

--