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.

Challenge 1: Nested heterogeneous collections

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.

The solution: Centralise the Class mapping

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…

Challenge 2. Heterogeneous collections as return types

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.

The solution: A wrapper class!

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.

Conclusion

Downloads

Kewin Dannerfjord Remeczki

Written by

iOS Developer from 🇩🇰. Full time 💻📲 — part time 🚴🏽‍⛷🏃🏽‍🏊🏼‍