How to set custom CodingKey for the convertFromSnakeCase decoding strategy

⋅ 7 min read ⋅ Swift

Table of Contents

Swift uses Camel case naming convention, but not all back-end languages use the same convention. Ruby, for example, uses Snake case when naming variables. This naming convention usually reflects on JSON API key naming.

Here is an example of a snake case API response.

[
{
"id": 1,
"node_id": "MDQ6VGVhbTE=",
"url": "https://api.github.com/teams/1",
"html_url": "https://github.com/orgs/github/teams/justice-league",
"name": "Justice League",
"slug": "justice-league",
"description": "A great team.",
"privacy": "closed",
"permission": "admin",
"members_url": "https://api.github.com/teams/1/members{/member}",
"repositories_url": "https://api.github.com/teams/1/repos",
"parent": null
}
]

So, you probably encounter both camelCase and snake_case naming in your API response.

Luckily, Apple provided an easy way to convert from snake to camel case with .convertFromSnakeCase decoding strategy. In this article, I will explore the .convertFromSnakeCase decoding strategy and highlight things you should know when using it with a custom coding key.

What is the convertFromSnakeCase decoding strategy

convertFromSnakeCase is a decoding strategy that converts snake-case keys to camel-case keys.

For demonstration, I will try to convert a snake-case JSON to a User struct with camel-case naming.

Here is our User struct.

struct User: Decodable {
let firstName: String
let lastName: String
}

And here is a JSON string and how we decode to User struct.

let jsonString = """
{
"first_name": "John",
"last_name": "Doe"
}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

let user = try! decoder.decode(User.self, from: data)
print(user)
// User(firstName: "John", lastName: "Doe")

JSON has a snake case naming, but our user struct use camel case. Using .convertFromSnakeCase will handle the key conversion for us.

How convertFromSnakeCase do the conversion

Behind the scene, this strategy follows these three steps to convert JSON keys to camel-case:

  1. Capitalize each word that follows an underscore.
  2. Remove all underscores that aren’t at the very start or end of the string.
  3. Combine the words into a single string.

Normally, you shouldn't need to care about these rules, but if you have some fancy naming which the above rules do not meet your expectation, these rules are crucial when you try to do a custom key conversion.

There are two namings that I want to highlight here.

  1. Name with underscores.
  2. Name with inconsistent capital letters.

Underscore

There is some unclear behavior for rules number one and two. That's why I want to make it more clear in this section.

Capitalize each word that follows an underscore.

This only applies to underscore between words, not at the very start and end of the string.

The following examples show the result of applying this strategy:

// 1
_private converts to _private not _Private
// 2
__private converts to: __private not __Private

1 Letter p follows an underscore, but it doesn't get converted to _Private.
2 Underscore at the very start can be any numbers. In this case, we have two underscores, and p is not capitalized.

Remove all underscores that aren’t at the very start or end of the string.

For this rule, underscores at the very start and end can be in any numbers. As long as they are not between words, they are all kept.

The following examples show the result of applying this strategy:

// 1
___leading_ converts to ___leading_, not _leading_

// 2
_trailing___ converts to _trailing___, not _trailing_

// 3
first__name converts to firstName

// 4
last___name converts to lastName

1 Underscore at the very start can be any numbers. In this case, we have three underscores, and all of them are preserved.
2 The same rule applies to underscores at the very end.
3, 4 Regardless of numbers, all underscores between each word will be removed.

I don't think this will be a problem since you probably name your key with many underscores, but it is good to know the rule since you need to use it when doing a custom key naming.

Inconsistant captial letters

This is more likely to happen, and you might want to take a closer look at this problem.

From rule number one, the .convertFromSnakeCase strategy only capitalizes each word, and it can't infer capitalization for acronyms or initialisms such as WYSIWYG or URI.

Capitalize each word that follows an underscore.

The following examples show the result of applying this strategy:

avatar_url converts to avatarUrl.
customer_id converts to customerId.
html_string converts to htmlString.

This might not be a serious issue, but if you want it to be capitalized the way you want, you need to specify a custom CodingKey. This is where your knowledge about the converting rules is put into use.

Custom CodingKey

For demonstration, let create a new struct with acronym and initialisms capitalized.

struct SnakeToCamel: Decodable {
let avatarURL: String
let customerID: String
let HTMLString: String
}

If you try to decode the following JSON String to SnakeToCamel struct, you will get a key not found decoding error.

let jsonString = """
{
"avatar_url": "Avatar URL",
"customer_id": "Customer ID",
"html_string": "HTML String"
}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

let snakeToCamel = try! decoder.decode(SnakeToCamel.self, from: data)
print(snakeToCamel)

You will get an error that looks like this:

Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "avatarURL", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "avatarURL", intValue: nil) ("avatarURL"), with divergent representation avatarUrl, converted to avatar_url.", underlyingError: nil))

This error comes from the fact that our Swift variable key not matched the JSON key. If you use Codable before, you know you can have a different mapping between JSON key and Swift variable name. You can do this with the help of CodingKey.

Problem

Let's try to add a custom coding key for our mapping.

We add a coding key to map between JSON key and our property names in the following example.

struct SnakeToCamel: Decodable {
let avatarURL: String
let customerID: String
let HTMLString: String

enum CodingKeys: String, CodingKey {
case avatarURL = "avatar_url"
case customerID = "customer_id"
case HTMLString = "html_string"
}
}

Try to decode again, and you will get a similar error. You get the same key not found error, but the key is the one you just specified (avatar_url) this time.

Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "avatar_url", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "avatar_url", intValue: nil) ("avatar_url").", underlyingError: nil))

Solution

The solution is to set a coding key to the value that matches the one after it gets converted by the .convertFromSnakeCase decoding strategy. In this case, it would be the following:

avatar_url converts to avatarUrl.
customer_id converts to customerId.
html_string converts to htmlString.

So, our coding key should look like this.

struct SnakeToCamel: Decodable {
let avatarURL: String
let customerID: String
let HTMLString: String

enum CodingKeys: String, CodingKey {
case avatarURL = "avatarUrl"
case customerID = "customerId"
case HTMLString = "htmlString"
}
}

Try to decode again. This time it should be successfully decoded.

You can easily support sarunw.com by checking out this sponsor.

Conclusion

When you set a custom coding key, you have to map your property name to the value after converted with a specified decoding strategy. So to properly do it with .convertFromSnakeCase, you have to understand how it converts from snake case to camel case. That is what we learned in How convertFromSnakeCase do the conversion.


Read more article about Swift or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
How to define custom environment values in SwiftUI

It might not be obvious that we can create a custom environment value, but we can do that. The steps to create one are not as straightforward as we usually do, but it isn't hard if you know how to do it.

Next
How to make a custom button style with UIButton.Configuration in iOS 15

Learn how to create an outline button style.

← Home