Swift's SingleValueDecodingContainer and JSON arrays

I ran into an interesting bit of JSON while working on a new project. I was having trouble figuring out how to parse it with Codable:

The content array here contains multiple different types of objects. But in Swift, you need to define very specific Codable structs (or classes, or enums) to decode to. So, how do you parse this JSON into objects in Swift?

The first thing that might come to mind is class inheritance. All of the objects have common data: the type field. Subclass for concrete implementations of each type of object and voilá! You implement a custom Decoder init and you’re done. While this would probably work, it makes me very sad 😢.

You’re actually hiding the concrete, valuable types underneath the umbrella base type, which is what you would end up passing around with your decoded struct. So, you end up with a lot of if let valuableStuff = baseObject as? ConcreteType hanging around your code. Ugly. I never liked inheritance because of stuff like this.

So, I ruled out the heavy hammer that is class inheritance as a viable solution to this problem. Swift gives us plenty of other tools to tackle this problem.

Embed Block
Add an embed URL or code. Learn more

Eventually I settled on using the power of structs, enums and the oft-ignored SingleValueDecodingContainer.

First, I defined some types:

So, we have our main Content struct for our base JSON object, which contains an Array of Element. Element is an enum with an associated value for each type of object in the content Array.

But wait, how does Swift encode/decode enums with associated values? Well, it doesn’t we have to do it ourselves. If we build this code now we get the error:

Type ‘Content.Element’ does not conform to protocol ‘Decodable’

Unfortunate. So, how we write decode and encode methods for Element? We have hit an oft-ignored part of JSON parsing in Swift. We would like to avoid writing anything horrendous (and this can get horrendous quickly), and we’d like to leverage as much magic Swift automatic synthesis as possible.

I used the fun fact we noticed while considering class inheritance: All of the JSON objects have a type field. So I defined a BaseContent type:

A few things here: I defined ContentType based on the values the type JSON field can take on. Also note the Codable implementation on ContentType, and that I defined a custom CodingKeys enum for BaseContent. It usually isn’t necessary to do this but it’ll become clear later why I did that. If you’re not familiar with the CodingKeys enum within the Codable system in Swift, you can read about it here.

Now when decoding our JSON, we can first decode the BaseContent, figure out what type we’re dealing with and then decode specifically for that type.

Here’s where SingleValueDecodingContainer comes into play. Once we know what type we’re working with, we are actually attempting to decode the entire object from our Decoder. In all of the examples I’ve seen around, even on Paul Hudson’s Codable guide, and in Apple’s own documentation, SingleValueDecodingContainer is only used to decode a primitive value like Int or String. But there is power in this little container! It comes with the following method:

func decode<T>(_ type: T.Type) throws -> T where T : Decodable

Which essentially means you can decode any Decodable type with this container. So what is SingleValueDecodingContainer? Let’s look at the other containers we have available to us to better understand the Decoding system available in Swift.

  1. KeyedDecodingContainer. This is probably the most common container. It’s for keyed values, so objects in JSON. If you’ve ever seen a decode(_:forKey:) method call, you’re using a KeyedDecodingContainer.

  2. UnkeyedDecodingContainer. A little rarer. Swift usually uses this container internally to decode JSON arrays. It tends to be limited to a single type of data without a bunch of fussing.

  3. Finally, SingleValueDecodingContainer. I’ve written several Codable-focused Swift packages and I’d never seen this container before today. It’s used to decode all of the data within a Decoder to a single value. This is typically used to decode Dates in custom formats.

So let’s use our newly discovered knowledge that SingleValueDecodingContainer can use all of the data in a Decoder to create a single Decodable object to use by writing an init for Element:

That’s it! Step by step we’re

  1. Decoding a BaseContent object from our Decoder

  2. Extracting the type value from the BaseContent

  3. Creating a SingleValueDecodingContainer from the same Decoder (which gives us the same data)

  4. switching over type to determine which struct we need to decode into Element.

Here we can see why we defined CodingKeys on BaseContent. We need to access it’s CodingKeys outside of BaseContent and by default CodingKeys enums are private.

Alright! This code with successfully decode our JSON array. Let’s wrap up by writing our encode method. Here’s the finished code in all it’s glory:

Using a JSONDecoder, the JSON provided at the top of this post successfully encodes into a Content instance, and using JSONEncoder, Content encodes into the same JSON.

While a little code heavy at the encoding and decoding methods, I feel like this method of implementing Codable makes a lot of sense, successfully leverages Swift’s type system by not hiding types, and is easy to expand upon if new array types need to be added.

Pros

  • Uses Swift’s type system heavily

  • Keeps Encoding and Decoding code for array elements out of parent

  • Easily expandable

  • Allows you to ensure you handle all Element cases by using switch statements

Cons

  • Decodes data in a Decoder twice

  • A decent amount of code to implement something seemly simple

  • switch statements can be annoying if you’re not into them

  • Uses some obscure parts of the Codable system, not much reading out there on this topic.

Enjoy!

If you can think of any way to improve this process, or have any comments on the code or writing of the post, feel free to leave a comment or email me at emma@emma.sh