Safely Decoding Enums in Swift

Tammo Freese
Mobimeo Technology
Published in
7 min readApr 30, 2021

In Swift, we can make most types decodable by adding the Decodable protocol. While Swift’s default decoding is sufficient in most cases, it leads to issues if simple enumerations are extended later on.

This article demonstrates different approaches how to work around this issue, and highlights the advantages and disadvantages of each approach.

The Problem

Let’s say we are working on an app that allows you to book and pay for different modes of transportation, and we would like to configure from the backend which transport modes and payment methods are shown, and in which order.

We could have an enum for the transport modes, and one for the payment methods:

The app config we get from the backend could then include two arrays of these enums:

To reduce the noise in the upcoming examples, we introduce a helper method to decode a string containing JSON to a given type:

Let’s make sure everything works as expected:

In new versions of the app, we could add new cases to the enums. We may extend the supported transport modes with a case rideHailing, and/or the payment methods with a case sepa. If our backend sends the new cases to the old version, decoding of the app config will fail:

We could make sure our backend does only send the new values to the newer clients, still that’s a risk: accidentally sending a payload with just one unexpected value for any of the enums will break the decoding of the whole app config. This is different to adding additional keys in the JSON object, which are ignored in decoding.

Let’s explore options on how to work around the issue. The goal is to skip unknown enum cases when decoding.

Solutions Using Custom Decoding

When we add Decodable to a type, we get a default implementation of the decoding initializer, that is a method with the signature init(from decoder: Decoder) throws .

Custom Decoding Initializer

Providing our own decoding initializer gives us complete control over how decoding is handled. Inside the initializer, we first decode our enums to arrays of the underlying raw value type, then use compactMap to get rid of any values for which we have no mapping in our enums. To implement this custom decoding, we have to provide an enum for the coding keys as well:

Decoding now skips the unknown enum cases like we wanted:

The disadvantage of this solution is that whenever we add properties to the app config, we have to change it in two places now: add the property to CodingKeys, and decode it by hand in init.

Parse underlying Raw Values and Compute the Arrays

Instead of defining a custom decoder, we can use the CodingKeys to map the arrays to private properties with the respective underlying raw value type, then compute the enum array properties based on the private properties:

Decoding gives us the same results as before:

The solution is quite straightforward, and we don’t need a custom initializer anymore. We still have to declare the CodingKeys though, so when adding new properties to the app config, we need to add these to the coding keys. Also, the properties are computed which carries a performance penalty, and forces us to declare them with var instead of let .

We could work around the repeated computation by using lazy properties:

But then accessing the lazy properties mutates the struct, so when accessing the properties, the decoded app config itself needs to be declared with var instead of let:

All approaches we saw so far share the disadvantage of duplicating the logic for mapping the enums. Let’s explore solutions on the property level.

Solutions on Property Level

We want the properties with the enum arrays to be decoded differently to the default behavior. To achieve that, we can use a custom type as the property, instead of using an array directly.

Wrap the Property in a Custom Type

We put the logic for skipping unknown cases in a custom decoding initializer for our wrapper type:

In the app config, we use our custom wrapper type instead of arrays:

After decoding, we now need to access the values property of our custom type:

Compared to the solutions before, there are a couple of advantages: We don’t have to define CodingKeys or a custom initializer on the app config, so when adding properties to the app config we don’t need additional code changes. Also, we removed the duplicated logic of ignoring the unknown values — there is only one compactMap instead of two. We can reuse this wrapper type wherever we like.

One disadvantage is the wrapper type bleeds out to all usages of the properties: Instead of appConfig.transportModes or appConfig.paymentMethods, we have to use appConfig.transportModes.values or appConfig.paymentMethods.values.

Use a Property Wrapper

In newer versions of Swift, we can declare a property wrapper instead.

Note there is not much change to the struct we had before — we add @propertyWrapper, and rename the variable containing the value to wrappedValue.

The types of the properties are back to being arrays of enums:

Accessing the decoded properties works directly again, no more .values:

A slight drawback is we have to declare the properties asvar, as properties using property wrappers cannot be declared as let.

Solutions on Type Level

The previous two solutions handled the skipping of unknown values at the property level. It may be more practical to skip on the type level though: Whenever we declare a property of type [TransportMode] or [PaymentMethod] in a decodable type, we may want to get the skipping of unknown cases for free, without having to use a property wrapper each time.

Extend KeyedDecodingContainer

To change the decoding behavior for decoding a type for a key, we can extend KeyedDecodingContainer. That type makes the encoded properties of a decodable type accessible by keys.

Among other methods KeyedDecodingContainer has methods of the signature decode(_, forKey: _). If we add overloads of this signature for more special types than are covered by the existing decode methods, they will be used in generated decoding initializers.

To not interfere with the code of the other examples, we define the extension for new types TransportMode5, PaymentMethod5, and AppConfig5:

We add two extensions, one for each enum array type:

And let’s make sure decoding succeeds:

When the decoding initializer for AppConfig5 is generated, our two customedecode methods are used as they define how to decode the types [TransportMode5] and [PaymentMethod5].

A disadvantage is we ended up with code duplication again. Also, there is no indication—neither on the properties nor on the types—that arrays of transport modes and payment methods are handled in a non-default way.

Make the KeyedDecodingContainer Extension Reusable

Let’s make the extension reusable. First, we declare a protocol to mark the types for which we want to skip unknown cases in array decoding. We only allow the protocol to be added on types that fulfill our current requirements: We want a type that conforms to both RawRepresentable and Decodable, and the raw value needs to be decodable as well:

Then, we extend KeyedDecodingContainer with a decode method that applies only to arrays of types conforming to our protocol:

Now, all we have to do is to let the types conform to our protocol.

And unknown enum cases are skipped again when decoding:

Which Solution to Prefer?

We saw six approaches on how to solve the issue. Which one should we prefer? I think there are a few we can rule out as candidates:

  • Custom decoding (solutions 1+2) forces us to write extra code when adding properties to the app config.
  • Of solutions on the property level (solution 3+4), I prefer the property wrapper: while it needs the change from let to var, it does not influence how call sites look.
  • Of the solutions on type level with KeyedDecodingContainer (solutions 5+6), I prefer making the extension reusable: it has no code duplication, and the protocol at least gives a hint that the type is handled in a special way.

Two solutions remain: on property level using a property wrapper (solution 4), and on type level using a marker protocol (solution 6).

I like the marker protocol solution because it declares our intent on the type: if we have multiple places where an array of a certain enum is returned, we only need one declaration on the type, and not use the property wrapper multiple times. So no duplication. On the other hand, we may want the duplication in this case: the changed decoding is explicitly visible on the property itself.

If I would have to decide for one winner, I would go for the property wrapper, but the marker protocol is a close second.

Whichever solution you prefer in your code, if you end up needing more and more of workarounds for Swift’s default decoding, there are libraries out there which may help. One example is AirBnB’s ResilientDecoding, others are just an internet search away.

Bonus Track: Increase Reusability of the Marker Protocol Solution

Still here? Good! You may have noticed we had to add an awful lot of constraints on the SkipUnknownCasesInArrayDecoding protocol. Wouldn’t it be nice if we could apply it to any decodable type?

Turns out this is possible. We can use the nestedUnkeyedContainer(forKey:) method to get a data structure that allows us to go through all elements in the array. We collect all elements where the decoding succeeds in a result array. The elements where decoding fails we skip by decoding them to an empty struct — there decoding always succeeds:

And that’s it for the bonus track. Happy Decoding!

--

--