Enums with Decodable in Swift

Jushrita
The Startup
Published in
5 min readMay 10, 2020
Photo by Dmitry Ratushny on Unsplash

I had a very basic knowledge of Codable which was limited to how to convert JSON data into primitive types such as String, Int, Bool, etc. or how to work with nested JSON objects and Arrays. I have been using it for quite a long time and it was sufficient for the project code to work. But Codable is more powerful than this.

In this article, we will discuss a scenario where the JSON we receive has an enum type. Earlier if I had received an enum(which means the value for this key would be within a set of values) in response, I would have converted it to primitive type first, and later in the presenter or view-model layer, I would have converted it to an enum. I am sure most of you would have done the same.

Now, let’s learn how to convert data to enum in the network layer itself.

Let’s make our network layer more powerful.

Enums With Decodable

Let’s consider the following JSON for example:

{
"order": {
"id": 12678,
"status": "Shipped",
"item": {
"id": 1209,
"handleWithCare": false
}
}
}

Here, status can hold a pre-defined set of values like Pending, Shipped, Approved, etc. Generally, I would have kept the type for status as String while parsing and later on I would have converted it into an enum. So my JSON model would look something like this:

struct OrderResponse : Decodable {
var order: Order?
}
struct Order: Decodable {
var id: Int?
var status: String?
var item: Item?
}
struct Item: Decodable {
var id: Int?
var handleWithCare: Bool?
}

status can easily be converted into an Enum type as shown below:

struct OrderResponse: Decodable {
var order: Order?
}
struct Order: Decodable {
var id: Int?
var status: OrderStatus?
var item: Item?
}
struct Item: Decodable {
var id: Int?
var handleWithCare: Bool?
}
enum OrderStatus: String, Decodable {
case pending = "Pending"
case approved = "Approved"
case shipped = "Shipped"
case delivered = "Delivered"
case cancelled = "Cancelled"
}

You might be thinking, so what’s a big deal in that? How you have created a struct for Item, you could have created an enum with raw values for status. This is what I have done in the example as well.

This would work perfectly in an ideal situation where the value of status is anything from this pre-defined set only. Now consider a case where this code is in the production environment and what if backend has decided to add a new value, InProcess to this set. You would handle the new value in the existing code. But what will happen to the production app? In this scenario, the JSON data will not be parsed and hence will error out with Swift.DecodingError.dataCorrupted.

How to resolve this problem?

To solve this, we have to make changes to the enum which we have just created. Add an unknown case with an associated type. As associated values cannot have raw values, you have to remove all the raw values. Now your enum would look something like this:

enum OrderStatus: Decodable {
case pending, approved, shipped, delivered, cancelled
case unknown(value: String)
}

Now, there is an issue here. Since your parser doesn’t know which JSON key should be mapped to which case, Xcode will throw Type ‘OrderStatus’ does not conform to protocol ‘Decodable’ error when you try to compile the code. Let’s go ahead and add the stub:

enum OrderStatus: Decodable {
case pending, approved, shipped, delivered, cancelled
case unknown(value: String)
init(from decoder: Decoder) throws {
}
}

You have to add the mapping inside the initializer. But how to do that? You have to use a container for that. Codable provides two containers: KeyedDecodingContainer and SingleValueDecodingContainer. KeyedDecodingContainer is used when you are working with keys. You need to pass the keys to the container and it will return the value for the respective keys. As the name suggests SingleValueDecodingContainer can be used when you are not working with key-value pairs, which is the current scenario.

We will create a SingleValueDecodingContainer and retrieve the String data. Now, this data could be mapped to different enum cases as shown below:

enum OrderStatus: Decodable {
case pending, approved, shipped, delivered, cancelled
case unknown(value: String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let status = try? container.decode(String.self)
switch status {
case "Pending": self = .pending
case "Approved": self = .approved
case "Shipped": self = .shipped
case "Delivered": self = .delivered
case "Cancelled": self = .cancelled
default:
self = .unknown(value: status ?? "unknown")
}
}
}

Now build and run the code. You can see that the errors are gone and the status is parsed into OrderStatus type. Now go ahead and change the value of status in JSON to InProcess. Now status is mapped correctly to the unknown case with value InProcess: unknown(value: “InProcess”). With this even if new values are sent in status, your production code will not break and work just fine. You just need to handle generic unknown(value) case in the app.

The entire code would look like this:

let testJson = """
{
"order": {
"id": 12678,
"status": "InProcess",
"item": {
"id": 1209,
"handleWithCare": false
}
}
}
""".data(using: .utf8)
struct OrderResponse : Decodable {
var order: Order?
}
struct Order: Decodable {
var id: Int?
var status: OrderStatus?
var item: Item?
}
struct Item: Decodable {
var id: Int?
var handleWithCare: Bool?
}
enum OrderStatus: Decodable {
case pending, approved, shipped, delivered, cancelled
case unknown(value: String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let status = try? container.decode(String.self)
switch status {
case "Pending": self = .pending
case "Approved": self = .approved
case "Shipped": self = .shipped
case "Delivered": self = .delivered
case "Cancelled": self = .cancelled
default:
self = .unknown(value: status ?? "unknown")
}
}
}
if let orderResponse = try? JSONDecoder().decode(OrderResponse.self, from: testJson!) {
print(orderResponse)
}

Go ahead, paste this code in the playground, and try it yourself. You can try different values for the status and check if it is working fine or not.

I hope this was helpful. Thank you!

--

--