:codable (karma)

A mobile engineer trying to figure out how to be a good father

Creating a custom Decodable key decoding strategy

We've all been there, you just get round to integrating your application with a new and exicting API.

But wait, where we use camel case in Swift the API doesn't. This means that creating your Decodable structure like the following:

struct Assets: Decodable {
    let coverSmall: String
    let coverMedium: String
    let logo: String
}

Wouldn't quite work as Decodable property names doesn't match the keys within the JSON

{
    "cover-small": "www.small-cover-url.com",
    "cover-medium": "www.medium-cover-url.com",
    "logo": "www.logo.com"
}

Which would result in the error DecodingError.keyNotFound...

So what do we do?

Something we can all relate to is creating a custom CodingKeys enum which matches the keys of the JSON to the properties

enum CodingKeys: String, CodingKey {
    case coverSmall = "cover-small"
    case coverMedium = "cover-medium"
    case logo
}

However...

What if there was an easier way then replicating CodingKey enums within every Decodable structure you create? What if we create a custom JSON key decoding strategy?

By default JSONDecoder.keyDecodingStrategy only has three values:

  • useDefaultKeys The default value which uses the key declared against each Decodable type
  • convertFromSnakeCase Coverts keys from snake_case_keys to camelCaseKeys and then try mapping the key to a property on the type
  • custom Takes a closure to convert the JSON key to another format, and then uses that String value within a custom CodingKey to match it to the properties on the type
    ### So using our earlier example

If we create a custom CodingKey structure which conforms to the CodingKey protocol

struct KebabCaseCodingKey: CodingKey {
  
  var stringValue: String

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

  var intValue: Int? {
    return nil
  }

  init?(intValue: Int) {
    return nil
  }

}

Then we can use String manipulation, to convert a String from the initial format to our desired ones. Which matches the properties on our Decodable structure.

// Split the string on the `-` delimiter which also removes it from the String
let kebabCaseString = "kebab-case-string".split(separator: "-")
let keyStrings = keyValues.enumerated().map { index, string in
    // We want to leave the first SubString and return it as a string
    guard index > 0 else {
        return String(string)
    }
    
    // return the rest of SubStrings as a String turning it into a camel case string
    return String(string).capitalized
}
.joined()

// keyStrings = "kebabCaseString"

We can use that String manipulation on the keys that are passed through the keyDecodingStrategy to change them from the initial format to our desired one, before returning an instance of our KebabCodingKey

let decoder = JSONDecoder()

decoder.keyDecodingStrategy = .custom({ keys in
    let lastKey = keys.last! 
    if lastKey.intValue != nil {
        return lastKey
    }
    
    let keyValues = lastKey.stringValue.split(separator: "-")
    let keyStrings = keyValues.enumerated().map { index, string in
        guard index > 0 else {
            return String(string)
        }
        return String(string).capitalized
    }.joined()
    
    return KebabCaseCodingKey(stringValue: keyStrings)!
})
Tagged with: