DEV Community

Ross Butler
Ross Butler

Posted on

Get More from Codable by Implementing a JSON Key Decoding Strategy

If you’re not using these, you’re probably writing more code than necessary

Codable is a protocol that was introduced in Swift 4 to make the serialization and deserialization of data into and out of Swift structures a breeze. This post assumes some knowledge of how to use Codable so if you’re unfamiliar with the basics it’s worth reading Encoding and Decoding Custom Types first as a primer. With that being said, let’s continue.

Imagine you have the following JSON you wish to deserialize:

Here’s the series of Swift structures you might write to deserialize the data:

Then using JSONDecoder decode the data as follows:

Very quickly you’ll run into the following error message:

keyNotFound(CodingKeys(stringValue: travelsOn, intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: vehicles, intValue: nil), _JSONKey(stringValue: Index 0, intValue: 0)], debugDescription: No value associated with key CodingKeys(stringValue: \travelsOn\, intValue: nil) (\travelsOn\)., underlyingError: nil))

That’s because the JSONDecoder is looking for the the key travelsOn in your JSON file. Unfortunately, the actual name of the key in the JSON file is travels-on.

No problem, we can solve this by implementing CodingKeys and custom conformance to the Codable protocol as follows:

We’ve implemented our own initializer and encode function in order deserialize / serialize using the JSON keys provided by our CodingKeys enum. But that’s quite a bit of extra code - had our JSON keys been in snake case as follows then it would have made life much easier:

This is because in this instance, we could have specified a JSON key decoding strategy and avoided have to write all of that extra code:

This one line of code specifying a keyDecodingStrategy saved us from having to implement an initalizer, encode function and CodingKeys enum.

Unfortunately our original JSON file uses kebab case rather than snake case keys and Foundation doesn’t provide a key decoding strategy for kebab case.

However, it does provide the custom key decoding strategy:

case custom(([CodingKey]) -> CodingKey)

This allows us the ability to implement our own custom key decoding strategy for decoding keys in whatever format we wish. All we need to do is provide an implementation of the custom key decoding strategy returning an implementation of the CodingKey protocol (we’ve named our implementation AnyKey following the example provided in the Apple documentation):

Note that the CodingKey protocol defines two failable initializers which must be implemented:

init?(intValue: Int)
init?(stringValue: String)

However we’ve provided an additional non-failable intializer to make our implementation a little more straightforward.

When implementing a custom key decoding strategy, you must provide a closure which accepts an array of type `CodingKey. An array is passed to the closure rather than a single key in order to provide all of the ancestors of the current key providing some context for the current key to be decoded in case this affects the decoding strategy in any way. The current key will always be the last key in the array.

For example, when decoding the key number-of-wheels in our original JSON example, the array passed to the closure would look as follows:
▿ 3 elements

  • 0 : CodingKeys(stringValue: "vehicles", intValue: nil) ▿ 1 : _JSONKey(stringValue: "Index 0", intValue: 0)
  • stringValue : "Index 0" ▿ intValue : Optional
  • some : 0 ▿ 2 : _JSONKey(stringValue: "number-of-wheels", intValue: nil)
  • stringValue : "number-of-wheels"
  • intValue : nil

The last key is the actual key we are interested in however note that the previous key in the array is a JSON key with an int value of 0. This is because the number-of-wheels key belongs to a JSON object at position 0 in the array of vehicles objects.

You might have wondered why we needed to provided an implementation of the initializer init?(intValue: Int) as part of the CodingKey protocol - this is so that we are able to index arrays which aren’t usually indexed by strings.

Using our new key decoding strategy, all we need write to decode our original JSON file is:

{% gist https://gist.github.com/rwbutler/6d5bfad21d4edd62c41dbf470c257f6d %}

The new key coding strategy can be applied every time we need to use the Codable protocol to deserialize from JSON into a Swift structure from now on making it worth the initial investment of writing a custom key decoding strategy.

However, if you’d rather make use of some pre-written implementations then LetterCase provides a number of implementations for converting keys from a number of popular letter cases including kebab case, train case and macro case amongst others:

  • convertFromCapitalized
  • convertFromDashCase
  • convertFromKebabCase
  • convertFromLispCase
  • convertFromLowerCase
  • convertFromLowerCamelCase
  • convertFromMacroCase
  • convertFromScreamingSnakeCase
  • convertFromTrainCase
  • convertFromUpperCase
  • convertFromUpperCamelCase

LetterCase is an open-source framework available under MIT license compatible with Cocoapods, Carthage and Swift Package Manager. As well as providing implementation of JSONDecoder.KeyDecodingStrategy for decoding JSON it provides implementations of JSONEncoder.KeyEncodingStrategy for going the other way and encoding JSON keys.

It also provides an implementation for converting from one letter case to another therefore you if you wanted to decode the original JSON file:



Into variables of macro case e.g.

Then this could be achieved as follows:

Summary

Hopefully this article has shown that by spending a little bit of time upfront creating a custom JSON key decoding strategy, a lot of time can be saved further down the road by reusing the same strategies each and every time you need to decode JSON keys of a different letter case.


LetterCase can be found open-sourced on GitHub under MIT license and is compatible with both Cocoapods, Carthage and Swift Package Manager.

Top comments (0)