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.

Hi halleygen, which kind of solution did you use at the end?

Sorry for late reply. I haven't been active here for a while :sweat_smile:

I originally used an Item protocol with separate structs for each kind of API, which worked fine, but came with some (unforeseen by me) drawbacks that negatively affected my productivity such as:

  • The Item protocol required associated types as well as conformance to Identifiable and Equatable which meant I had to make other objects and functions that interacted with Items generic. This made it a bit harder to understand the code, but the main issue was it noticeably increased build times and the frequency with which SourceKit would crash.
  • Having an Item type for each API created quite a bit of boilerplate code because they all needed their own property definitions and initialisers, when in reality each of the types was identical save for the logic used to decode it. This made it frustrating to maintain, especially because the refactoring tools often just didn't work.

Instead in my situation what I've found to be a better solution is to have an API protocol that contains the decoding logic and an Item struct (instead of a protocol) which is generic over the API. Eg:

protocol API {
    associatedtype CodingKeys: CodingKey
    static func decodeItem(from container: KeyedDecodingContainer<CodingKeys>) throws -> Item<Self>
}

struct Item<Site: API>: Hashable, Decodable {
    let str: String
    let int: Int
    let date: Date
    
    init(str: String, int: Int, date: Date) {
        self.str = str
        self.int = int
        self.date = date
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Site.CodingKeys.self)
        self = try Site.decodeItem(from: container)
    }
}

enum SiteA: API {
    enum CodingKeys: String, CodingKey {
        case str, int, date
    }
    
    static func decodeItem(from container: KeyedDecodingContainer<CodingKeys>) throws -> Item<Self> {
        Item(
            str: try container.decode(String.self, forKey: .str),
            int: try container.decode(Int.self, forKey: .int),
            date: try container.decode(Date.self, forKey: .date)
        )
    }
}

enum SiteB: API {
    enum CodingKeys: String, CodingKey {
        case str, int
        case date = "time"
    }
    
    static func decodeItem(from container: KeyedDecodingContainer<CodingKeys>) throws -> Item<Self> {
        guard let int = Int(try container.decode(String.self, forKey: .int)) else {
            throw DecodingError.dataCorruptedError(forKey: .int, in: container, debugDescription: "Was not an integer string")
        }
        
        return Item(
            str: try container.decodeIfPresent(String.self, forKey: .str) ?? "default value",
            int: int,
            date: try container.decode(Date.self, forKey: .date)
        )
    }
}

This has been much more successful for me because it has simplified my generic constraints to the point that Xcode no longer struggles with building/autocompleting/refactoring and reduces the amount of boilerplate code needed. I understand that a lot of work is being done on improving generics and protocols with associated types in Swift so perhaps this will change in the future.

1 Like

Use serializedSwift. It has an alternate key option. It works like this:

import SerializedSwift

class ClassName: Serializable {

@Serialized(alternateKey: "_id")

var id: String?