Customizing how Codable objects map to JSON data
Published on: April 5, 2021In the introductory post for this series you learned the basics of decoding and encoding JSON to and from your Swift structs. In that post, you learned that your JSON object is essentially a dictionary, and that the JSON's dictionary key's are mapped to your Swift object's properties. When encoding, your Swift properties are used as keys in the encoded JSON output dictionary.
Unfortunately, we don't always have the luxury of a 1 to 1 mapping between JSON and our Swift objects.
For example, the JSON you're working with might use snake case (snake_case
) instead of camel case (camelCase
) for its keys. Of course, you could write your Decodable
object's properties using snake case so they match the JSON you're decoding but that would lead to very unusual Swift code.
In addition to the case styling not being the same, your JSON data might even have completely different names for some things that you don't want to use in your Swift objects.
Fortunately, both of these situations can be solved by either setting a key decoding (or encoding) strategy on your JSONDecoder
or JSONEncoder
, or by specifying a custom list of coding keys that map JSON keys to the properties on your Swift object.
If you'd prefer to learn about mapping your codable objects to JSON in a video format, you can watch the video on YouTube:
Automatically mapping from and to snake case
When you're interacting with data from a remote source, it's common that this data is returned to you as a JSON response. Depending on how the remote server is configured, you might receive a server response that looks like this:
{
"id": 10,
"full_name": "Donny Wals",
"is_registered": false,
"email_address": "[email protected]"
}
This JSON is perfectly valid. It represents a single JSON object with four keys and several values. If you were to define this model as a Swift struct, it'd look like this:
struct User: Codable {
let id: Int
let full_name: String
let is_registered: Bool
let email_address: String
}
This struct is valid Swift, and if you would decode User.self
from the JSON I showed you earlier, everything would work fine.
However, this struct doesn't follow Swift conventions and best practices. In Swift, we use camel case so instead of full_name
, you'll want to use fullName
. Here's what the struct would look like when all properties are converted to camel case:
struct User: Codable {
let id: Int
let fullName: String
let isRegistered: Bool
let emailAddress: String
}
Unfortunately, we can't use this struct to decode the JSON from earlier. Go ahead and try with the following code:
let jsonData = """
{
"id": 10,
"full_name": "Donny Wals",
"is_registered": false,
"email_address": "[email protected]"
}
""".data(using: .utf8)!
do {
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: jsonData)
print(user)
} catch {
print(error)
}
You'll find that the following error is printed to the console:
keyNotFound(CodingKeys(stringValue: "fullName", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"fullName\", intValue: nil) (\"fullName\").", underlyingError: nil))
This error means that the JSONDecoder
could not find a corresponding key in the JSON data for the fullName
.
To make our decoding work, the simplest way to achieve this is to configure the JSONDecoder
's keyDecodingStrategy
to be .convertFromSnakeCase
. This will automatically make the JSONDecoder
map full_name
from the JSON data to fullName
in struct by converting from snake case to camel case.
Let's look at an updated sample:
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let user = try decoder.decode(User.self, from: jsonData)
print(user)
} catch {
print(error)
}
This will succesfully decode a User
instance from jsonData
because all instances of snake casing in the JSON data are automatically mapped to their camel cased counterparts.
If you need to encode an instance of User
into a JSON object that uses snake casing, you can use a JSONEncoder
like you would normally, and you can set its keyEncodingStrategy
to convertToSnakeCase
:
do {
let user = User(id: 1337, fullName: "Donny Wals",
isRegistered: true,
emailAddress: "[email protected]")
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(user)
print(String(data: data, encoding: .utf8)!)
} catch {
print(error)
}
The output for this code is the following:
{"id":1337,"full_name":"Donny Wals","email_address":"[email protected]","is_registered":true}
The ability to automatically convert from/to snake case is really useful when you're dealing with a server that uses snake case instead of camel casing.
Using a custom key decoding strategy
Since there is no single standard for what a JSON response should look like, some servers use arbitrary patterns for their JSON keys. For example, you might encounter service that uses keys that look like this: USER_ID
. In that case, you can specify a custom key encoding- or decoding strategy.
Let's take a look at a slightly modified version of the JSON you saw earlier:
{
"ID": 10,
"FULL_NAME": "Donny Wals",
"IS_REGISTERED": false,
"EMAIL_ADDRESS": "[email protected]"
}
Since these keys all follow a nice and clear pattern, we can specify a custom strategy to convert our keys to lowercase, and then convert from snake case to camel case. Here's what that would look like:
do {
let decoder = JSONDecoder()
// assign a custom strategy
decoder.keyDecodingStrategy = .custom({ keys in
return FromUppercasedKey(codingKey: keys.first!)
})
let user = try decoder.decode(User.self, from: uppercasedJson)
print(user)
} catch {
print(error)
}
The closure that's passed to the custom
decoding strategy takes an array of keys, and it's expected to return a single key. We're only interested in a single key so I'm grabbing the first key here, and I use it to create an instance of FromUppercasedKey
. This object is a struct that I defined myself, and it conforms to CodingKey
which means I can return it from my custom key decoder.
Here's what that struct looks like:
struct FromUppercasedKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
// here's the interesting part
init(codingKey: CodingKey) {
var transformedKey = codingKey.stringValue.lowercased()
let parts = transformedKey.split(separator: "_")
let upperCased = parts.dropFirst().map({ part in
return part.capitalized
})
self.stringValue = (parts.first ?? "") + upperCased.joined()
self.intValue = nil
}
}
Every CodingKey
must have a stringValue
and an optional intValue
property, and two initializers that either take a stringValue
or an intValue
.
The interesting part in my custom CodingKey
is init(codingKey: CodingKey)
.
This custom initializer takes the string value for the coding key it received and transforms it to lowercase. After that I split the lowercased string using _
. This means that FULL_NAME
would now be an array that contains the words full
, and name
. I look over all entries in that array, except for the first array and I captialize the first letter of each word. So in the case of ["full", "name"]
, upperCased
would be ["Name"]
. After that I can create a string using the first entry in my parts
array ("full"
), and add the contents of the uppercase array after it (fullName
). The result is a camel cased string that maps directly to the corresponding property in my User
struct.
Note that the work I do inside init(codingKey: CodingKey)
isn't directly related to Codable
. It's purely string manipulation to convert strings that look like FULL_NAME
to strings that look like fullName
.
This example is only made to work with decoding. If you want it to work with encoding you'll need to define a struct that does the opposite of the struct I just showed you. This is slightly more complex because you'll need to find uppercase characters to determine where you should insert _
delimiters to match the JSON that you decoded initially.
A custom key decoding strategy is only useful if your JSON response follows a predicatable format. Unfortunately, this isn't always the case.
And sometimes, there's nothing wrong with how JSON is structured but you just prefer to map the values from the JSON you retrieve to different fields on your Decodable
object. You can do this by adding a CodingKeys
enum to your Codable
object.
Using custom coding keys
Custom coding keys are defined on your Codable
objects as an enum
that's nested within the object itself. They are mostly useful when you want your Swift object to use keys that are different than the keys that are used in the JSON you're working with.
It's not uncommon for a JSON response to contain one or two fields that you would name differently in Swift. If you encounter a situation like that, it's a perfect reason to use a custom CodingKeys
enum to specify your own set of coding keys for that object.
Consider the following JSON:
{
"id": 10,
"fullName": "Donny Wals",
"registered": false,
"e-mail": "[email protected]",
}
This JSON is slightly messy, but it's not invalid by any means. And it also doesn't follow a clear pattern that we can use to easily transform all keys that follow a specific pattern to something that's nice and Swifty. There are two fields that I'd want to decode into a property that doesn't match the JSON key: registered
and e-mail
. These fields should be decoded as isRegistered
and email
respectively.
To do this, we need to modify the User
struct that you saw earlier in this post:
struct User: Codable {
enum CodingKeys: String, CodingKey {
case id, fullName
case isRegistered = "registered"
case email = "e-mail"
}
let id: Int
let fullName: String
let isRegistered: Bool
let email: String
}
The only think that's changed in this example is that User
now has a nested CodingKeys
enum. This enum defines all keys that we want to extract from the JSON data. You must always add all keys that you need to this enum, so in this case I added id
and fullName
without a custom mapping. For isRegistered
and email
, I added a string that represents the JSON key that this property should map to. So isRegistered
on User
will be mapped to registered
in the JSON.
To decode an object that uses custom coding keys, you don't need to do anything special:
do {
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: jsonData)
print(user)
} catch {
print(error)
}
Swift will automatically use your CodingKeys
enum when decoding your object from JSON, and it'll also use them when encoding your object to JSON. This is really convenient since there's nothing you need to do other than defining your CodingKeys
.
Your CodingKeys
enum must conform to the CodingKey
playform and should use String
as its raw value. It should also contain all your struct properties as its cases. If you omit a case, Swift won't be able to decode that property and you'd be in trouble. The case itself should always match the struct (or class) property, and the value should be the JSON key. If you want to use the same key for the JSON object as you use in your Codable
object, you can let Swift infer the JSON key using the case name itself because enums will have a string representation of case as the case's raw value unless you specify a different raw value.
In Summary
In this post, you learned how you can use different techniques to map JSON to your Codable
object's properties. First, I showed you how you can use built-in mechanisms like keyDecodingStrategy
and keyEncodingStrategy
to either automatically convert JSON keys to a Swift-friendly format, or to specificy a custom pattern to perform this transformation.
Next, you saw how you can customize your JSON encoding and decoding even further with a CodingKeys
enum that provides a detailed mapping for your JSON <-> Codable conversion.
Using CodingKeys
is very common when you work with Codable
in Swift because it allows you to make sure the properties on your Codable
object follow Swift best practices without enforcing a specific structure on your server data.