Even though they are sometimes overused, enums are still a great tool. Thanks to Codable
they can easily be decoded from JSON sent by your backend.
This is great until you need to add a new case to the enum. Then you have to either deal with versioning your API or you need to tell your users to update the app. In some cases you have no choice, but in other cases it would be fine to just ignore the new value and fall back to some default behavior.
Here are a few ways I used to realize such a scenario:
RawRepresentable
struct
The simplest way would be to replace enums with a RawRepresentable
struct:
struct ItemType: RawRepresentable, Codable {
var rawValue: String
static let house = ItemType(rawValue: "house")
static let tree = ItemType(rawValue: "tree")
}
Code language: JavaScript (javascript)
In code this can be used just like an enum would, you’d only have to add a default
case to every switch statement.
This can lead to situations where a different subset of those cases is handled in different places. Sometimes that’s fine, but in other cases you want to ensure to handle all known cases everywhere. So you need an actual enum in order for the compiler to be able to do exhaustiveness checks.
Enum with “Unknown” case
To do this we simply can manually implement the decoding and map unknown values to a default case:
enum ItemType: String, Codable {
case house
case tree
case unknown
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
switch string {
case "house": self = .house
case "tree": self = .tree
default: self = .unknown
}
}
}
Code language: JavaScript (javascript)
Implementing this for many enums becomes pretty boring. Luckily we can share the implementation between all enums by writing a protocol with a default implementation:
protocol UnknownDecodable: Decodable, RawRepresentable {
static var unknown: Self { get }
}
extension UnknownDecodable where RawValue: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let raw = try container.decode(RawValue.self)
self = Self(rawValue: raw) ?? .unknown
}
}
Code language: JavaScript (javascript)
Using this we can define our enum like we would normally and get that behavior for free by just conforming to the UnknownDecodable
protocol. Note that our enum case unknown
can fulfill the protocol requirement of static var unknown: Self
:
enum ItemType: String, Codable, UnknownDecodable {
case house
case tree
case unknown
}
Code language: JavaScript (javascript)
Decoding unknown cases as nil
A final solution I sometimes find useful is decoding the enum as optional and mapping unknown values to nil
.
This is easy to do if we manually implement decoding for the struct that contains this enum. But this would lead to a lot of (possibly repeated) boilerplate code we don’t like to write. Instead we can use a property wrapper:
@propertyWrapper
struct DecodeUnknownAsNil<Enum: RawRepresentable> where Enum.RawValue: Codable {
var wrappedValue: Enum?
}
extension DecodeUnknownAsNil : Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let raw = try container.decode(Enum.RawValue.self)
wrappedValue = Enum(rawValue: raw)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue?.rawValue)
}
}
extension KeyedDecodingContainer {
func decode<Enum>(_ type: DecodeUnknownAsNil<Enum>.Type, forKey key: Key) throws -> DecodeUnknownAsNil<Enum> {
return try decodeIfPresent(type, forKey: key) ?? .init(wrappedValue: nil)
}
}
extension DecodeUnknownAsNil: Equatable where Enum: Equatable {}
extension DecodeUnknownAsNil: Hashable where Enum: Hashable {}
Code language: JavaScript (javascript)
The extension on KeyedDecodingContainer
is necessary to handle missing or null
values in the JSON.
When to use which
I choose the RawRepresentable
struct when I don’t need to make a decision based on that value. This could be for looking up some resource or other values in a dictionary.
The DecodeUnknownAsNil
property wrapper is great if the enum value is optional anyways and you can handle the unknown case just as if there was no value at all.
For most cases I would chose the UnknownDecodable
protocol though. If I make my property optional I can distinguish between no value and an unknown value. And if the property is not optional I know the decoding will fail so I can catch bugs on the server side where required properties are missing.