Swift’s Codable protocol (together with Encodable and Decodable) was introduced in Xcode 9 with Swift 4. This changed for good how we encode and decode JSON. In this article I’m hoping to show you how to make this experience better, when dealing with JSON keys in other formats.

Encoding/Decoding JSON with camelCase keys

If we are lucky, the JSON we work with will follow Swift naming conventions.

Names of types and protocols are UpperCamelCase. Everything else is lowerCamelCase.

Swift API Design Guidelines - Conventions

This convention uses lower camel case (camelCase) for object/struct property names. In this scenario, we can encode/decode JSON objects straight out of the box, without any modifications.

Given the following Swift structure:

struct Device: Codable {
    let deviceName: String
    let deviceModel: String
}

And the following JSON:

{
    "deviceName": "iPhone 12 Pro",
    "deviceModel": "iPhone"
}

We can decode it as follows:

let device = try JSONDecoder().decode(Device.self, from: json)
print(device.deviceName) // iPhone 12 Pro

Nothing new here, pretty straightforward 😉

Encoding/Decoding JSON with snake_case keys

Snake case is the standard naming convention in some programming languages, like Python and Ruby. Thus, is pretty common to see snake_case used for JSON properties.

Fortunately, Swift includes an out-of-the-box solution to parse JSON in this format. JSONDecoder.KeyDecodingStrategy includes a convenient convertFromSnakeCase option (see convertToSnakeCase for JSONEncoder)

Given the following JSON:

{
    "device_name": "iPhone 12 Pro",
    "device_model": "iPhone"
}

We can decode it as follows:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let device = try decoder.decode(Device.self, from: json)
print(device.deviceName) // iPhone 12 Pro

Pretty cool. One line of code, and we are all set 🎉

Encoding/Decoding JSON with CuSToM-CaSe keys

What about other common cases? While lowerCamelCase and snake_case might be the two most popular formats for JSON object keys, some programming languages might have different conventions.

For example, C# uses PascalCase for property names, and it’s common for C# APIs to return JSON in this format.

Example JSON with PascalCase keys:

{
    "DeviceName": "iPhone 12 Pro",
    "DeviceModel": "iPhone"
}

A common way to handle these keys is by defining a CodingKeys enumeration conforming to String & CodingKey, as shown in Apple documentation examples and many other articles.

Following this pattern, we would update our Swift structure as follows:

struct Device: Codable {
    let deviceName: String
    let deviceModel: String

    enum CodingKeys: String, CodingKey {
        case deviceName = "DeviceName"
        case deviceModel = "DeviceModel"
    }
}

By defining these custom keys, we can decode it as before:

let device = try JSONDecoder().decode(Device.self, from: json)
print(device.deviceName) // iPhone 12 Pro

This is great, but… it has obvious downsides. For large data objects and specially large APIs, it can be tedious and repetitive to type down the coding keys for each property. Imagine having hundreds of properties among dozens of data transfer objects. Lots of boilerplate. While there are generator tools that can generate these coding keys for you, it is still not ideal.

My recommendation would be to define coding keys only when working with inconsistent APIs and JSON payloads. This is, when there is a mix of formats and some properties use different formats. Sadly, this happens, and there are many APIs out there that lack any sense of consistency. In these scenarios, manually defining coding keys is the way to go.

So, what is the alternative?

Depending on the JSON key format, we could update our Swift structure to match, as long as the format is valid Swift code (hyphens are not allowed in property names, but underscores are).

Here is an example for PascalCase:

struct Device: Codable {
    let DeviceName: String
    let DeviceModel: String
}

An another example for snake_case:

struct Device: Codable {
    let device_name: String
    let device_model: String
}

Both of these are valid Swift code, but we are breaking naming conventions and introducing inconsistency in our code. There are cases where this is totally fine. However, in many cases, this would be considered bad practice and, if you have a linter or code formatter, you might find yourself adding rule exceptions.

How can we do better?

Fortunately, both JSONEncoder and JSONDecoder allow for defining custom key encoding/decoding strategies. This can be a little tricky the first time, but it is worth the effort, specially if, as mentioned, the API you are working with is pretty consistent in a given format.

Defining Custom Key Decoding Strategies

Going back to the above scenario where the JSON has keys in PascalCase format, we can define a custom strategy as follows. Here is our original bare structure, without manually defined coding keys:

struct Device: Codable {
    let deviceName: String
    let deviceModel: String
}

To define a custom strategy, first we will define a custom CustomKey type. This is because the strategy must return a type that conforms to CodingKey (same concept as the enumeration), and we cannot make instances of a protocol.

struct CustomKey: CodingKey {
    let stringValue: String
    let intValue: Int?

    init(stringValue: String) {
        self.stringValue = stringValue
        intValue = nil
    }

    init(intValue: Int) {
        self.intValue = intValue
        stringValue = String(intValue)
    }
}

Then we can use it to decode any type as follows:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom { keys in
    let last = keys.last!.stringValue
    return CustomKey(stringValue: last.prefix(1).lowercased() + last.dropFirst())
}
let device = try decoder.decode(Device.self, from: json)
print(device.deviceName) // iPhone 12 Pro

Our custom strategy converts from PascalCase to camelCase to map any properties from C# naming convention to Swift.

The take away in this example is that we only need to define this once for all our data models and API endpoints consumed. As we develop our applications, we can keep adding and updating our data models without having to manually type or generate coding keys.

We can take this a step further by defining a specific PascalCaseKey type to encapsulate our conversion logic:

struct PascalCaseKey: CodingKey {
    let stringValue: String
    let intValue: Int?

    init(stringValue: String) {
        self.stringValue = stringValue.prefix(1).lowercased() + stringValue.dropFirst()
        intValue = nil
    }

    init(intValue: Int) {
        stringValue = String(intValue)
        self.intValue = intValue
    }
}

Then we can use it as follows:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom { keys in PascalCaseKey(stringValue: keys.last!.stringValue) }
let device = try decoder.decode(Device.self, from: json)
print(device.deviceName) // iPhone 12 Pro

This is a very efficient way of defining a custom strategy for decoding JSON keys. As long as you can convert your keys to match your Swift property names, you can implement this for any key format.

Extending JSONDecoder with Custom Key Decoding Strategies

If you are working on a framework or library, or simply would rather provide a simple solution for your application, it is possible to extend JSONDecoder (and JSONEncoder) to add custom key decoding/encoding strategies.

Based on the examples above, we can write this extension:

extension JSONDecoder.KeyDecodingStrategy {
    static var convertFromPascalCase: Self {
        .custom { keys in
            PascalCaseKey(stringValue: keys.last!.stringValue)
        }
    }
}

And use it as follows:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromPascalCase
let device = try decoder.decode(Device.self, from: json)
print(device.deviceName) // iPhone 12 Pro

Hurray! No more manually defining coding keys for our data models 🎉

Using this technique we can create API clients, persistence layers or any other code that works with JSON, and fully encapsulate and abstract the JSON key format, without exposing it to the rest of the application and without defining coding keys for each model property.


This article was written as an issue on my Blog repository on GitHub (see Issue #23)

First draft: 2021-01-13

Published on: 2021-01-14

Last update: 2021-01-14