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
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 Codable
protocol.
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 Cat
and Dog
, allowing a Person to have a single collection of superclass type Pet
with 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 Pet
objects, loosing all subclass properties. Replacing Pet
with either Cat
or Dog
is 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
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 ClassFamily
protocol 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 Pet
case:
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 Person
dramatically:
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
…What if the heterogeneous collection is not nested within a Decodable
object? Consider, for instance, the case where the Pets
collection of the Person
is 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 Decoder
object 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!
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 Decodable
for 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 ClassFamily
enum with the Discriminator
in 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:T
being the ClassFamily
enum andU
being 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 static
in the ClassFamily
protocol (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 AnyObject
type which is not decodable.
The result is a ClassWrapper
object that now holds the correctly mapped object. This can now be used in the getPets()
function of our Person
class:
Cool, it works! But the generic ClassWrapper
object 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 JSONDecoder
class:
A benefit of this JSONDecoder
extention is that all other classes no longer need to know about the ClassWrapper
object, hence this class can now be made private
within the extension.
With this icing on the cake we are able to decode the heterogeneous list of Pet
objects like this:
This “wraps” up the solution. In order to support other families of objects, we simply just have to implement a new ClassFamily
enum 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
In this article we’ve seen how heterogeneous collections can prove to be a real challenge when using the Swift 4.0 Decodable
protocol. 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.
Downloads
If you’re interested, the full playground for this article can be downloaded by clicking here.