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
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
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
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
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.
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
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
Doing so allows us to clean up the initialiser of
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
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
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!
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:
Ubeing 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
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
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
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.