Swift JSON Decoding: A brief look into Decoders

John Arnaoutakis
4 min readApr 27, 2021

Decoding JSON objects became easy with the introduction of the Decodable protocol. By creating a simple struct that conforms to the Decodable protocol we can decode a JSON object and use the decoded data in our application.

Let’s take a look at a simple JSON object.

{
"first_name": "John",
"last_name": "Appleseed",
"age": 26
}

Let’s say that this object describes a user, it gives us the first name, last name and the age of the user. To decode this object in swift we can create the struct below:

struct User: Decodable {
let first_name: String
let last_name: String
let age: Int
}

As you can see all you need to do is match the JSON key to a property in the struct and provide the expected type. That simple!

It is common to use a camel case naming convention and as you can see the properties first_name & last_name don’t follow that convention. For example, we would prefer to have the properties first_name & last_name declared as firstName and lastName. We can easily achieve that by creating an enum that matches the key in the JSON object with the property in our struct. This is where the CodingKey protocol comes into play.

The CodingKey protocol allows us to use a type as a key for encoding and decoding.

struct User: Decodable {   enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case age
}
let firstName: String
let lastName: String
let age: Int
}

By declaring the CodingKeys enumeration we are defining which key should be assigned to which property during the decoding process.

Even though the age property didn’t change and is exactly the same as declared in the struct, we need to specify that property in our CodingKeys enumeration as well. If we don’t we’ll get the following error:

Type 'User' does not conform to protocol 'Decodable'

During the decoding process every property must have a value by the end of it. It’s like creating a constructor (init) method that doesn’t initialise every single property of the struct. The initialisation would be incomplete and you’ll get a compilation error. This is exactly what you would expect since the age property doesn’t have a value assigned to it.

Let’s dive in a little deeper and create our own initialisation process. Let’s assume that we want to decode our object but we don’t want to have an exact, one-to-one, match for every property. For example we want to keep the first_name and last_name in a single property called fullName. Then the User object would look like this:

struct User: Decodable {   enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case age
}
let fullName: String
let age: Int
}

This again would give us a compilation error like the one we discussed before since there isn’t an enumeration case for the fullName property, hence the fullName isn’t initialised. In this particular case we have to create our own initialisation process. We’ll use the init(from decoder: Decoder) constructor method to do this.

struct User: Decodable {  enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case age
}
let fullName: String
let age: Int
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let firstName = try container.decode(String.self, forKey: .firstName)
let lastName = try container.decode(String.self, forKey: .lastName)
fullName = [firstName, lastName].joined(separator: " ")
age = try container.decode(Int.self, forKey: .age)
}
}

The init(from decoder: Decoder) constructor method allows us to have access to a Decoder. A Decoder has the data stored in it and in order to access it we need to do some more steps.

First we need to get a container. There are three different types of decoding containers. This is the official documentation for each one of them:

  1. KeyedDecodingContainer: A concrete container that provides a view into a decoder’s storage, making the encoded properties of a decodable type accessible by keys.
  2. SingleValueDecodingContainer: A container that can support the storage and direct decoding of a single nonkeyed value.
  3. UnkeyedDecodingContainer: Returns the data stored in this decoder as represented in a container appropriate for holding values with no keys.

I’ll write a different story that get’s more in depth for each container.

In this case we want to use a KeyedDecodingContainer to access each key and decode its value. To get this container we pass the CodingKeys to specify the keys that the container has. Ιn our case the keys are: first_name, last_name & age. To pass the keys we simply use the enumeration that we declared before.

let container = try decoder.container(keyedBy: CodingKeys.self)

The next step is to get the first_name value from the container by specifying to the decode method that the expected value is of type String and that the key is the firstName case from the CodingKeys enumeration.

let firstName = try container.decode(String.self, forKey: .firstName)

We do the same for the lastName in the next line.

let lastName = try container.decode(String.self, forKey: .lastName)

Since we got both values, we can finally join those values and assign the transformed value to our property fullName.

fullName = [firstName, lastName].joined(separator: " ")

Finally since we are creating our own decoding process we should also make sure that the age property gets its decoded value and that’s what we do in the last line. The age value is of type Int and we specify that in the decode method. That’s it!

age = try container.decode(Int.self, forKey: .age)

To sum up, we learned how to declare a decodable object, how to take advantage of the CodingKey protocol and use an independent naming for our properties and also how to create our own decoding process. If you enjoyed this story and you would like to see more don’t forget to clap!

--

--