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
struct
s (or classes
, or enum
s) 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.
Eventually I settled on using the power of struct
s, enum
s 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 enum
s 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.
KeyedDecodingContainer
. This is probably the most common container. It’s for keyed values, soobject
s in JSON. If you’ve ever seen adecode(_:forKey:)
method call, you’re using aKeyedDecodingContainer
.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.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 aDecoder
to a single value. This is typically used to decodeDate
s 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
Decoding a
BaseContent
object from ourDecoder
Extracting the
type
value from theBaseContent
Creating a
SingleValueDecodingContainer
from the sameDecoder
(which gives us the same data)switch
ing overtype
to determine whichstruct
we need to decode intoElement
.
Here we can see why we defined CodingKeys
on BaseContent
. We need to access it’s CodingKeys
outside of BaseContent
and by default CodingKeys
enum
s 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 usingswitch
statements
Cons
Decodes data in a
Decoder
twiceA decent amount of code to implement something seemly simple
switch
statements can be annoying if you’re not into themUses 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