Use multiple kinds of coding keys to decode the same object

Hi!

Suppose I have a model like the following that is decoded from JSON:

struct Item: Decodable {
    let name: String
    let identifier: UUID
}

The JSON can come from various web APIs. While the JSON data from each different place all represent an Item and contain a name and an identifier, they don't always use the same keys.

How would be the best way to decode Items from these different JSON data objects?

Two of the things I've tried are:

Using if statements in init(from:) to choose the right keys:

init(from decoder: Decoder) throws {
    if api_alpha {
        let container = try decoder.container(keyedBy: AlphaKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        ...
    } else if api_beta {
        let container = try decoder.container(keyedBy: BetaKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        ...
    } else if api_gamma ...
    ...
}

But these becomes unwieldy the more properties the model has/the more APIs there are, plus there is a lot of repetition and every time I an API is added or removed I'd have to update the code.

Create a wrapper around a simple "string key" type, and put the wrapper in the decoder's user info dictionary

struct StringKey: CodingKey {
    let stringValue: String
    init(stringValue: String) {
        self.stringValue = stringValue
    }
    ...
}

struct ItemKeys {
    enum Key {
        case name, identifier
    }

    let dictionary: [Key: StringKey]

    subscript(_ key: Key) -> StringKey {
        return dictionary[key]!
    }
}

struct Item: Decodable {
    ...
    init(from decoder: Decoder) throws {
        let codingKeys = decoder.userInfo[wrapperKey] as! ItemKeys
        let container = try decoder.container(keyedBy: StringKey.self)
        self.name = container.decode(String.self, forKey: codingKeys[.name])
        ...
    }
}

This works fine, but the compiler can't check if a key is missing from the wrapper, so I have to do it myself.

Is there a better/standard way to do solve this problem?

With Swift 5.3 and SE-0280 I thought that maybe there's now someway of doing something like this:

protocol ItemCodingKey: CodingKey {
    static var name: Self { get }
    ...
}

enum AlphaItemKeys: String, ItemCodingKey {
    case name = "name"
}

enum BetaItemKeys: String, ItemCodingKey {
    case name = "n"
}
...

let decoderForDecodingFromAlpha = JSONDecoder()
decoderForDecodingFromAlpha.userInfo[userInfoKey] = AlphaItemsKeys.self

let decoderForDecodingFromBeta = JSONDecoder()
decoderForDecodingFromBeta.userInfo[userInfoKey] = BetaItemsKeys.self

But since the ItemCodingKeys has Self requirements, I'm not sure how I'd get it back out from the user info dictionary.

I would have an ItemProtocol and struct ItemAlpha, struct ItemBeta that adopt ItemProtocol. Your design has the requirements that there are different types that are related. This should be modeled by different structs that adopt the same protocol. Most likely you can use the default decoding and things should be simplified.

You'd need to configure userInfo dictionaries of those decoders somewhere and that setup code would end up looking much like if/else branching you're trying to avoid.

An assumption that different APIs will be kept in-sync appears to be… naive. At some point later you'll most likely require some conditional transformations, type unwinding etc.

I'd define a generalized type for the my application and a type for each API representation. This solution is not concise but much easier to support.

Thanks for your response! I think I was overthinking it. This looks to be the best way of doing it.

Terms of Service

Privacy Policy

Cookie Policy