Swift Heterogeneous Codable Array
02 Jan 2019Quite the mouthful of a title but nevertheless it’s a typical problem. Receiving data from a remote service is super common but it’s not always obvious how to represent our data in a strongly typed language like Swift.
Problem outline
Let’s imagine an example where we are using a remote service that
returns a collection of shapes. We have struct
s within our app that
represent the various shapes and we want to parse the JSON objects into these native
types.
Here’s the struct
definitions:
and our JSON feed looks like this:
A first attempt at a solution
Our initial attempt to parse this might end up creating a new type called
FeedShape
that has optional attributes for every possible shape. We can
use JSONDecoder
to parse the feed. Then as a second step we can map the shapes
into our native types. That might look like this:
Whilst this will work it’s really not pleasant to write/maintain or use.
There are many issues with the above:
1) Optionals everywhere
Every time a new type is added that we can support within the app our Attributes
struct will grow. It’s a code smell for
there to be a type where most of its properties will be nil
.
2) Manually checking requirements before creating objects
In order to create the concrete types we have to manually check the type
property and that all the other required properties have been decoded.
The code to do this is not easy to read, this fact is painful because this code ultimately is the source of truth for how to decode these objects.
Looking at the current Attributes
type we can see that all it’s properties are Double?
- it could be quite easy to copy and paste the property
checking logic and end up trying to use the wrong key across multiple types.
3) Stringly typed code
To create the concrete types we need to check the type property against a String
. Having repeated strings scattered throughout a codebase is generally bad form
just asking for typos and refactoring issues.
4) We’ve lost the order
Due to the way the above is modelled there is no current way to keep track of the order in which the concrete types should actually appear.
5) It’s not taking advantage of our Codable
types
Our Square
and Rectangle
types already conform to Codable
so it would be beneficial to make use of this rather than manually
creating our types. Using Codable
also resolves the poor documentation issue raised in 2 because for simple types the compiler will generate
the Codable
implementation just from the type declaration.
Can we do better?
To make an improvement that addresses 2, 4 and 5 we can deserialise our collection to an [Any]
type. This requires a custom implementation of Decodable
in which we loop over the items and delegate the decoding to the Shape
/Rectangle
decodable implementations. The code looks like the following:
Although this is an improvement we still have stringly typed code and we’ve introduced another issue. Now we have an [Any]
type. The use of Any
can be a smell that we are not modelling things as well as we can do. This can be seen when we come to use the collection later on - we’ll be forced to do lot’s of type checking at run time. Type checking at run time is less desirable than at compile time because it means our app might crash in the wild as opposed to simply not compiling. There is also the issue that there is nothing at compile time that forces us to handle all cases e.g. I could very easily write code like this
Can we do better still?
The issues above can all be addressed.
In order to resolve 5 we need to create an array that can contain one type or another. Enums are the mechanism for creating the sum type we need, which gives us:
Issues 1, 2 and 5 can all be resolved by taking advantage of the fact that our types are already Codable
. If we
make our new Content
type Decodable
we can check the type we are dealing with and then delegate the decoding to the
appropriate Square
/Rectangle
decodable implementation.
NB: This is probably the trickiest transformation to follow, especially if you’ve not worked with custom decoding before. Just google any API you don’t recognise.
Finally to resolve 3 we can leverage the exhaustive checking of switch statements on enum
s.
By reifying the type
property from a String
to a real Swift type we convert run time bugs into compile time issues, which is always
a great goal to aim for.
NB: The Unassociated enum
might look a little odd but it helps us model the types in one concrete place rather than having
strings scattered throughout our callsites. It’s also quite useful in situations where you want to check the type of something
without resorting to case
syntax e.g. if we want to filter our collection to only Square
s then this is one line with our new Unassociated
type:
without the unassociated type this ends up being something like
Conclusion
The two key takeaways here are
-
If you need to represent a collection that can have multiple types then you’ll need some form of wrapper and
enum
s can perform that duty well when it makes sense. -
Swift’s
Codable
is really powerful and helped remove a heap of issues that arise from manually parsing/creating objects.
Removing optionality, reifying types and using compiler generated code are great ways of simplifying our code. In some cases
this also helps move runtime crashes into compile time issues, which is generally making our code safer. The benefits here are
great and it shows that it’s really worth taking time to model your data correctly and then use tools like Codable
to munge
between representations.
The title was a little bit of a lie as I only walked through the Decodable
part of Codable
(see the listing below for the
Encodable
implementation).
Full code listing
The full code to throw into a playground ends up looking like this: