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