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.
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