OMG, Codable is so frickin’ awesome

In my previous article, I described my brief foray with a scroll view, and there was this part:

To help you see what I saw, I went hunting on the Intertubes for a compelling public example of a data feed, and eventually settled on the USGS earthquake feed. Just. Because. To wire up the feed, I figured I’d give the new Swift Codable protocol a quick spin, and that’s when I learned how frickin’ awesome Codable is. But that is for the next article. Hold tight.

You can stop holding tight now. This is that article.

We’re going to go spelunking into Xcode 9 and Swift 4’s Codable capabilities. We’ll start with the GeoJSON feed example I alluded to in my article about scroll view, and then dive deeper into more examples using the RolePlayingCore Github repository.

To start: simply conform to Codable

I started with a basic struct that contained just a couple of items, and quickly built it up with nested structs, and enums, even. The GeoJSON feed is straightforward enough (see link here) that the automatic generation of constructors and coding keys provided by Swift magically worked. I wound up with this, which covers about half the properties, mostly the interesting ones:

Notice that there isn’t anything actually JSON-specific, or encoding/decoding-specific. Just that the struct and each embedded struct conforms to the Codable protocol. Codable is actually two conjoined protocols, Encodable & Decodable. The Swift compiler takes care of the rest, generating CodingKeys, an init(from:) initializer, and an encode(to:) function, automatically.

Note thatTsunami, Alert and Status, above, map strings and ints to enum values. Nice! And, the optional alert property, which is automatically handled as optional for decoding and encoding. Also, container types such as Array are also Codable for free.

To read this class (uh, I mean struct) from JSON, I added a fetch() static method that accepts a block on completion:

Notice line 10, that’s the important line. Kaapow!

What was so wicked amazing about that, to me, was, no additional code was required to encode and decode this to and from JSON. Calling decode() just worked. Wow! Well, not without some finessing with optionals, but that was a cakewalk compared to working with auto layout (see previous article).

This class (erm, struct) would get wired up to the MasterViewController via an objects array variable and viewDidLoad():

That was it. I won’t go into, ahem, detail about the MasterViewController, but it hands off the selected cell’s USGSEarthquakeData.Feature, and displays the contents in a DetailViewController. Which scrolls. At least now it does (rilly, see previous article). Here is the end result, scrolled:

Now, I didn’t try to tackle some of the potentially thornier issues that could arise from working with JSON. But that’s mainly because this example didn’t need it, and anyway, my journey to reveal deeper truths about Codable would soon be better spent with slightly more for-reals code, like the RolePlayingCore GitHub repository.

“All software becomes legacy as soon as it’s written.” — Andrew Hunt & David Thomas, The Pragmatic Programmer

OK, fine. That’s not for-reals code either, but since I had spent some time on JSON decoding in it already, I had built up quite a bit of “legacy” code over a shocking time span of six months, and it provided handy fodder for going deeper into Codable. So.

CodingKeys, Decoding, and Encoding, oh my!

The moment you have to deal with names that have underscores or spaces, flattening or unflattening nested types, or transforming non-native JSON types, such as poster-child Date, you suddenly have to add boilerplate to your Codables. It’s sufficiently straightforward though, that I almost want to skip over it, and get on with harder problems. But. Let’s.

Let’s start with an existing type that already works with JSON using serialization. So. Given some, let’s say, D&D class traits, we might have:

Note the TODO. OK. Call to action! Contribute to this open-source initiative! Help make me rich! Help me retire in comfort! OK, OK, back to reality…

The corresponding JSON format might look something like this:

In Swift 3, I might have defined JSON property keys, and this struct might have an initializer that accepts some JSON-deserialized dictionary of [String: Any]:

So much guard. So many angst.

In the above code, a Trait (not declared above) is a String, and static strings are provided to provide the conversion between lowercase space-separated JSON property names and their camelCaps counterparts in code. You know the drill if you’ve been doing something like this already, to type your keys.

So, because I had put spaces in the JSON property names, for Swift 4 Codable, I need to get explicit about CodingKeys as an equivalent of Traits. And, because I wanted to read some of them optionally, with fallback default values, I also need init(from:) and encode(to:):

Much cleaner. Soooo many blogs, articles, and web sites have already covered the basics of what is happening above. It’s not very interesting. Except… hold a tick.

Notice in the struct for ClassTraits, there’s Dice and there’s Ability. Wait! Dice is a protocol! (Have you seen my articles about Dice?) And, Ability is a… wait, oh, I haven’t covered that yet. OK. I’ll cover it now: that’s just a struct that holds a String. You know that drill.

But, Dice! Notice how in Codable above, Dice is eminently decodable, but somehow needs to be stringified when encoded. What’s up with that?

Let’s back up a step. This is an important step.

Encoding and decoding stringified types

Before we cover Dice, I want to cover Weight, Height and Currency. OK, not Currency just yet (maybe a separate article about that). Because the story of how to handle Dice started in those dark places. But to go there, we have to visit RacialTraits. Oh, jeez. OK. I forgot to cover that. RacialTraits.

First, the struct, what I ended up with (not exactly what the pre-Codable version had, but this is close enough):

OK, so there’s AbilityScores, Alignment, Height, Weight, and of course Dice. Before I got to Dice, I had to get through the others, to decode.

Alignment was easy, because it’s a struct of enums and Doubles. If you’re really curious, peruse the source code for Alignment; it’s not particularly interesting unless you’ve missed the 50 million articles about Codable, or the first handful of paragraphs in this article. Structs and enums just work when you simply make them conform to Codable.

Height and Weight were harder, because they are Foundation Measurement types of UnitLength or UnitMass, respectively, and those are types of NSCoding.

Ugh.

NSCoding hasn’t been reconciled with Codable yet; more importantly, in the scenarios above, I want height and weight to be like JSON Date, in that they are encoded in a string format, like “2 ft” and “3.2 kg”. I’ll focus on Height, to illustrate. Height is a measurement of unit length, and it can be parsed from a string, for feet, inches, and metric (at the scale of D&D):

OK. So with that, how to encode and decode? Well, it’s harder than I thought. I can’t easily make Height Codable, because it’s already NSCoding, and all kinds of hurt happen if I try. Plus I don’t want to, because I want height to be a formatted string in JSON, like “3 ft 2 in”. I know if I have a string parser to convert to Height, I can encode to string. So. How to decode? And, what if I want to support decoding unformatted height integers or doubles (in base units, feet)?

Alas! The type is being decoded from a KeyedDecodingContainer. So, if we provide an implementation for decode (and, don’t forget decodeIfPresent), for the Height.Type, it might just get found by the compiler:

And sure enough, that works.

I haven’t tried to implement the encode equivalent, but it’s easy enough to wrap the instance with “\(instance)”. So, that’s why decoding works directly for the type, but encoding uses a string.

OK. So, lather, rinse, and repeat for Weight.

Stringified protocols

Now, Dice. Trying to do the same for Dice doesn’t quite work. Part of the problem is, it’s a protocol, not a type. But if we do the same for it, replacing Protocol where Type appears in the above code, it actually also works:

OK! So, if I rely on stringifying on encoding, I can rely on decoding from string (or etc.) into specific types through decoding extensions.


Have I lost you yet? I have two more topics to cover. The first is a nested sub-type that is inside its parent (in JSON), and the second is named types.

Nested sub-types: Sub-races

When it comes to racial traits, in D&D there are sub-races for many of the core races. For example, the JSON for a race and two sub-races might look like this:

How to decode this? And, how will subraces inherit parent traits?

Well.

First, the RacialTraits type needs to implement an init(from:) initializer that can figure out what to do with the subraces. Here is what I came up with.

Notice several things. First, several of the properties have a default value even if the base class doesn’t have something to decode. Second, we pull a nestedUnkeyedContainer and decode subraces as an array of races, and then call blendTraits, to ensure that each subrace inherits its parent race’s properties. I couldn’t figure out a better way to do this in the time I gave myself. It appears that the unkeyed container requires using isAtEnd to know when to stop, and decode to peel off each element.

Careful: if you don’t use decode directly on the unkeyed container, it’ll never hit the end; I learned the hard way.

Here is how blendTraits is implemented, and how it interacts with some of the default values:

Jumping back to the struct declaration: the reason why so many properties have ! at the end of them is because they’re optional for decoding a subrace, but are required when used. blendTraits is what makes this OK, before actual clients use these types.

Not shown here: There is a wrapper for an array of RacialTraits, to peel out the sub races, so that races and subraces can be accessed together as a flat array. Also not shown is the encode function to only encode properties that are different between the parent and the subrace. View the source code in the GitHub project for more details on these and other refinements.

Custom keys in AbilityScores

OK so the other topic I wanted to cover was what happened to AbilityScores. Before Codable, these had a string name and dictionary to map the name to a value. After Codable, the string name became a CodingKey, and a Codable. Doiiiing!!!

That was easy enough. The rest felt like a bit of thrashing, until I got it right.

This solution is related to the scenario covered by the Apple Playground sample code for Using JSON with custom types (download this, I highly recommend going through the playground to learn more about Codable).

Here is the “before”:

TraitCoder was my hacky way to declare an init and encode function before Coding became a thing. Because Ability is a struct used as a dictionary Key, it’s also made to conform to Hashable, Equatable and Comparable, but that’s not shown here, because, space.

Here is the “after”:

Now, the Ability type implements Codable using singleValueContainer() of String, so that it doesn’t have to be declared with a name key in JSON, and so it can be used without AbilityScores. Also note that the AbilityKey is a struct with a var string, so that the Ability string… uh… can be mapped to and from it, as a CodingKey. There is more CodingKeys boilerplate involved because we can’t use an enum in this scenario. Then, we just iterate over all of the found AbilityKeys to make the dictionary of AbilityScores.

Whew. I almost lost myself, there.

So, now, if you specify something like “ability scores”: { “intelligence”: 2 }, in JSON, the value can be directly decoded into an AbilityScores of one element, whose Ability is “intelligence” and whose Int value (modifier) is 2.

What’s next?

Over in the repository, there’s also a little bit of latent wanna-use-a-NSKeyedArchiver going on in Player with RacialTraits and ClassTraits, and there’s also some more weirdness with Money, Currencies and UnitCurrency. More work to do. If you want to spelunk further into Codable, take a closer look inside RolePlayingCore.

Happy Codable-ing!