Customizing how Codable objects map to JSON data

Published on: April 5, 2021

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

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.

Categories

Codable