Support either kebab-case or PascalCase decoding by extending JSONDecoder

It is common to find RESTful APIs returning JSON payloads using either the kebab-case or PascalCase format. The first approach for tackling this situation may be writing the implementation of the CodingKey protocol conformance. The main disadvantage of going this way is that we would have to write the implementations for the CodingKey conformances of every Decodable model.

On the other hand, JSONDecoder allows setting the strategy to use to decode JSON data. It has two built-in strategies and a third option for custom writing:

  • useDefaultKeys - default strategy set
  • convertFromSnakeCase
  • custom(@Sendable (_ codingPath: [CodingKey]) -> CodingKey)

So, a different approach to achieving the custom decoding for either case would be writing an extension to do so.

convertFromKebabCase:

public extension JSONDecoder.KeyDecodingStrategy {
    /// A key decoding strategy that converts `kebab-case` keys to `camelCase` keys.
    static var convertFromKebabCase: JSONDecoder.KeyDecodingStrategy {
        .custom { codingPath in
            let codingKey = codingPath[codingPath.endIndex.advanced(by: -1)]

            guard codingKey.intValue == nil else {
                return codingKey
            }

            let components = codingKey
                .stringValue
                .components(separatedBy: "-")

            let head = components
                .prefix(1)
                .map(\.localizedLowercase)
            let tail = components
                .dropFirst()
                .map(\.capitalized)

            return type(of: codingKey)
                .init(
                    stringValue: (head + tail)
                        .joined()
                ) ?? codingKey
        }
    }
}

convertFromPascalCase:

public extension JSONDecoder.KeyDecodingStrategy {
    /// A key decoding strategy that converts `PascalCase` keys to `camelCase` keys.
    static var convertFromPascalCase: JSONDecoder.KeyDecodingStrategy {
        .custom { codingPath in
            let codingKey = codingPath[codingPath.endIndex.advanced(by: -1)]

            guard codingKey.intValue == nil else {
                return codingKey
            }

            let head = codingKey
                .stringValue
                .prefix(1)
                .lowercased()

            let tail = codingKey
                .stringValue
                .dropFirst()

            return type(of: codingKey)
                .init(stringValue: head + tail) ?? codingKey
        }
    }
}

Then, all we have to do is to create an instance of JSONDecoder, set the key decoding strategy, and use it, and that's it.