You won’t work with Codable, JSONDecoder and JSONEncoder for long before you run into this issue: the keys in your JSON payload don’t match up with the property names in your Codable type. Here’s a simple type:

struct Monster: Codable {
  let fullName: String
  let teeth: Int
}

And here’s the corresponding JSON payload:

{
  "FullName": "Scarious Groop",
  "Teeth": 45
}

In this case your API developers aren’t being evil, they’re just working to different standards. Other languages have capitalised property names, Swift uses property names starting with lower case. To deal with this, you can manually define a CodingKeys enumeration, specifying the name of the property and the name of the payload key:

private enum CodingKeys: String, CodingKey {
  case fullName = "FullName"
  case teeth = "Teeth"
}

This approach is fine for simple types but quickly becomes a maintenance headache when dealing with a larger number of properties. The advantages of Codable evaporate when you have to implement huge amounts of boilerplate.

To address this common issue, JSONDecoder and JSONEncoder each have a “strategy” that you can set to translate between JSON keys and property names. There is one built-in convenience strategy, .convertFromSnakeCase. This would work for the above example if the JSON payload looked like this:

{
  "full_name": "Wartslime McMaggot",
  "teeth": 3
}

If your payload isn’t using snake_case, then you’re left with the .custom strategy. This strategy uses a closure, which takes an array of CodingKeys and returns a single CodingKey.

The array of keys is provided to give you context - think of it as a key path from the root JSON object to where you are right now in the coding process. When decoding, the CodingKey instances supplied to you in this closure will be derived from the JSON field names. When encoding, they’ll come from the Codable types you’re using - they are synthesised by the compiler unless you’ve implemented your own CodingKeys enum as shown above. Each one will have a stringValue corresponding to the property name being encoded or decoded.

How, though, do you create and return a CodingKey? The only thing you’ve seen so far that is a CodingKey is the enum that you can define against a type - and if you have to define a whole new enum with the case-modified names, then we’re not saving anything compared to just defining keys with specific names.

CodingKey is a pretty simple protocol. It doesn’t have to be an enum - you can create a struct which implements it:

struct MyCodingKey: CodingKey {
  
  var stringValue: String

  init?(stringValue: String) {
    self.stringValue = stringValue
  }

  var intValue: Int? { 
    return nil 
  }

  init?(intValue: Int) {
    return nil
  }

}

You can initialise this struct with any string and that string will be used as the key in the JSON. There’s a slight wrinkle regarding the intValue - that’s needed to refer to each element in an array during the coding process. MyCodingKey doesn’t care about that - if you’re processing keys in the .custom strategy closure, and the key of interest has an intValue, then just return the original rather than making a new key.

The decoding closure can be written as follows:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom { keys in
  let lastKey = keys.last! // If only there was a non-empty array type...
  if lastKey.intValue != nil {
    return lastKey // It's an array key, we don't need to change anything
  }
  // lastKey.stringValue will be, e.g. "FullName"
  let firstLetter = lastKey.stringValue.prefix(1).lowercased()
  let modifiedKey = firstLetter + lastKey.stringValue.dropFirst()
  // Modified string value will be "fullName"
  return MyCodingKey(stringValue: modifiedKey)
}

The returned coding key will match the keys synthesised by the compiler on your Codable type. For the opposite direction, the code is almost identical, replacing lowercased() with uppercased() to go from your property name to the JSON field name:

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .custom { keys in
  let lastKey = keys.last! // If only there was a non-empty array type...
  if lastKey.intValue != nil {
    return lastKey // It's an array key, we don't need to change anything
  }
  // lastKey.stringValue will be, e.g. "FullName"
  let firstLetter = lastKey.stringValue.prefix(1).uppercased()
  let modifiedKey = firstLetter + lastKey.stringValue.dropFirst()
  // Modified string value will be "fullName"
  return MyCodingKey(stringValue: modifiedKey)
}

Implementing your own custom key strategy isn’t difficult and can save you a lot of boilerplate code if the transformation from JSON field name to property name is predictable.

Richard Turton

Cocoa Engineer

MartianCraft is a US-based mobile software development agency. For nearly two decades, we have been building world-class and award-winning mobile apps for all types of businesses. We would love to create a custom software solution that meets your specific needs. Let's get in touch.