Swift 4.2 Decodable: Heterogeneous collections 📚

Following last year’s release of the Codable protocol, many iOS-developers have been busy removing heavy, custom JSON parsers and replacing them with a smooth and lightweight conformation to Decodable, in the model layer… And I am no exception: In a recent project for a client, I had the joy of enhancing the model layer of a large application with one of the goals being a complete conformation to Codable.

The transition to Decodable initially went smooth (“happy days when you get to remove code!”). But the removal of the custom JSON parsers resulted in the removal of vital class-type mapping — something that is not directly supported by the otherwise powerful Codableprotocol.
To give an example of what I am talking about, consider the following:

In this very simple case we have a superclass, Pet,that is inherited by the two classes Catand Dog, allowing a Person to have a single collection of superclass type Petwith the actual objects in the collection being of either Pet, Cat or Dog types. The problem occurs when decoding a Person object from JSON, as the objects within the “pets” list are not of the same type:

The list of pets would be decoded within the Person initialiser like this: container.decode([Pet].self, forKey: .pets), but doing so will retrieve a list of the superclass Petobjects, loosing all subclass properties. Replacing Petwith either Cator Dogis not sufficient either, as we are interested in each of the distinct types including their respective properties.
At the same time deserialising the JSON (with JSONSerialization), to use the type discriminator for mapping, seem to completely remove the benefit of using Codable, as this was the way it was done with the old JSON parsers.

Luckily I was not alone with this problem: Tom Stoffer has written an excellent article on how to deal with heterogeneous lists nested inside Decodable objects. While this solution is good for one case, it is not a very clean solution for larger projects where we may encounter the same heterogeneous lists for several objects, as we would have to duplicate the type mapping code.
To deal with this we can extract the type information (mapping) to a centralised place for the family of classes. This can be done by utilising functions on swift enums. The goal is to create an enum that represents the family of classes that are related, by exposing a function for retrieving the correctly mapped type, as well as the key for the type discriminator in the JSON payload. As we want the solution to be as generic as possible, we can write a protocol to define the required exposed information:

Note: The Discriminator type is really only necessary in cases where you have different keys for the discriminator. (In the project I was working on I had two distinct keys). Furthermore the discriminator variable is static — more on this later.

The ClassFamilyprotocol allows us to create an enum that describes any family of objects. Let’s see an example of how such an enum would look like for our Petcase:

Great, this enum now describes our Pet object family!

We can then use this directly in the decodable initialiser of our Person class like so:

But to be honest, this solution has just extracted the mapping and not really made the initialiser cleaner.
So, to deal with this we will create a generic overload of the decode function in an extension for the KeyedDecodingContainer:

Doing so allows us to clean up the initialiser of Persondramatically:

We’re done with the solution to nested heterogeneous lists of decodable objects — nice and clean, isn’t it?

But that is, however, not all…

…What if the heterogeneous collection is not nested within a Decodable object? Consider, for instance, the case where the Petscollection of the Personis not a property, but is instead fetched from a server in an API call during runtime:

In this case the problem persists: The decoding of the[Pet]type will result in a list of unknown pets with names. Using Cat or Dog is not sufficient either and we do not have the Decoderobject with keyed containers as we do in the initialiser of the Person class, in order to use Tom Stoffer’s approach.
To my surprise I was not able to find much information of similar cases on Stackoverflow or elsewhere and going back to deserialising the JSON in order to read the discriminator type, followed by a decoding, was just not acceptable for me.

So, after some hours of messing with type inference problems and compile errors, I managed to come up with a generic solution using a wrapper class and some enums.

The idea was to be able to use the same approach as for the nested collections, by taking advantage of the same class family enum. In order to do so, we would have to create a wrapper class around the decodable class (and its subclasses), which we can then map to the correct type.
Let’s have a look at how we can implement this. The wrapper needs to be Decodablefor us to decode it directly with our JSONDecoder. Furthermore it needs to hold a reference to the object we wish to create. As we are working with generics, the object type should naturally also be generic.
To handle the mapping while being decoded we will implement the init(from decoder: Decoder)in which we will first decode the ClassFamilyenum with the Discriminatorin order to map the rest of the data to the correct type — just like before.
The implementation can be seen below:

Notice how the wrapper takes two generic types:Tbeing the ClassFamilyenum andUbeing the decodable superclass for the family of objects. To explain what is going on in the initialiser, we first get the container of the decoder as we normally would. We then try to decode the family (remember the enum corresponds to a value of the discriminator field “type”). Here we also notice why it was necessary to make the discriminator property staticin the ClassFamilyprotocol (we simply don’t have the enum in memory at this point).
With the class family enum in memory, we call getType()and cast the value to the type of the superclass U, in order to be able to call .init(from: decoder)on it. The cast is particularly important because the function returns the AnyObjecttype which is not decodable.
The result is a ClassWrapperobject that now holds the correctly mapped object. This can now be used in the getPets()function of our Personclass:

Cool, it works! But the generic ClassWrapperobject and the .compactMap have to be explicitly written every single time we want to decode a heterogeneous list — Not really handy. So let’s spice things up a bit by adding an extension to the JSONDecoderclass:

A benefit of this JSONDecoderextention is that all other classes no longer need to know about the ClassWrapperobject, hence this class can now be made privatewithin the extension.
With this icing on the cake we are able to decode the heterogeneous list of Petobjects like this:

This “wraps” up the solution. In order to support other families of objects, we simply just have to implement a new ClassFamilyenum that corresponds to the new context. Some of the advantages from using this approach is that it provides cleaner initialisers and a centralised location for the mapping. This is particularly useful to prevent code duplication if several classes would have heterogeneous lists of the same family.

In this article we’ve seen how heterogeneous collections can prove to be a real challenge when using the Swift 4.0 Decodableprotocol. But we’ve also seen how a little tweaking in the initialiser can solve the challenge for nested collections by using Tom Stoffer’s approach. Building on his approach we’ve seen a solution on how to centralise the object mapping in an enum. Furthermore I’ve presented the solution for the challenge of heterogeneous collections being returned directly. A generic wrapper class was useful when the collection is not nested within another decodable object.
Among the benefits of this solution are cleaner initialisers, less code duplication and proper code separation. The implementation has certainly improved my client’s project and I hope it can inspire you too.

If you’re interested, the full playground for this article can be downloaded by clicking here.

Lead iOS Engineer @ Jabra 🇩🇰 Full time 💻📲 — part time 🚴🏽‍⛷🏃🏽‍🏊🏼‍

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store