Swift 4.0 Codable — Decoding subclasses, inherited classes, heterogeneous arrays
Background
Swift 4.0 has introduced a really helpful API. Codable
If you have been following the Swift project and the new features in this version on the language you have probably already heard about it. I ran into some issues when working with some of our REST APIs so wanted to document them here in case other people run into the same situations.
Getting Started
If you are familar with Codable
you can skip this sections, if not this is how Apple summarise is on there docs page:
Make your data types encodable and decodable for compatibility with external representations such as JSON.
There is already quite a lot of good articles out there on how to use this new API. These are the best ones, I would suggest reading them as an entry point:
Encoding and Decoding Custom Types
Ultimate Guide to JSON Parsing With Swift 4
Swift 4 Decodable: Beyond The Basics 📦
Decoding Heterogeneous Arrays
A heterogeneous array is when you have a piece of JSON which contains multiple different objects. In my case, these objects are subclasses of a base class Drink
Each subclass has different attributes which need to be serialised.
Models
Firstly, the models need to be defined, conforming to Decodable
protocol. Starting with the base class and then the subclass.
This is the base class and defines all the attributes associated with it. There is nothing complicated about this, so no custom code needs to be written, it just uses the standard API.
Beer
has the additional attribute of alcohol_content
so we need to add that. This class needs to be initialised before the base class. We need to write our own custom init method. You can see on line 11, we pass the same decoder object to the super class so that it can decode the base classes attributes.
Serialisation
This is how we convert the json into memory. Nothing complicated here, but we now need to define the Drinks
object. This object contains a heterogenous array so we will need to implement the Decodable
protocol ourselves.
Heterogenous Array
Line 2 shows that we add an array of Drink
objects as a property to this class.
Next we create an enum DrinksKey
which is used to decode the dictionary containing the array of drinks.
We also define an enum DrinkTypeKey
which is used to serialise the type field for each object in the array.
And a final enum DrinkTypes
with specifies all the possible drink types.
With all the enums specified we can get to the serialisation.
Line 19: Get the drinks dictionary — container
Line 20: Get the array of drinks — drinkArrayForType
Line 23: Make a copy of the drinks array — drinksArray
This is needed as we be iterating the drinkArrayForType
In doing so we will have no way to get the decoder object for each element in the array. See summary on this below.
Line 24: Iterate the drinkArrayForType
container
Line 25: Get the current drink container
Line 26: Decode the type attribute
We can now use this decoded attribute to identify which class to decode. We have to use the copied drinksArray
object for decoding however, as the iterator of drinksArrayForType
has moved onto the next object. Viola!
Summary
This last step was the only way I could get the Decodable
API to do my bidding. As far as I can see there is no way to get a decoder from an UnkeyedDecodingContainer
or KeyedDecodingContainer
This seems like a limitation of the API, but Im sure there are good reasons for it not to be possible. It does seem a little strange to expose that dependency just for this use case of decoding.
If you think otherwise, let me know, it may be worth thinking about it a little further and adding a suggestion for improvement to the Swift API itself. After all this is only the first iteration of the this API, its possible developers may have missed this use case.