Jonas
Jonas

Reputation: 2249

Use multiple CodingKeys for a single property

Is it possible to use multiple CodingKeys for a single property?

struct Foo: Decodable {

    enum CodingKeys: String, CodingKey {
        case contentIDs = "contentIds" || "Ids" || "IDs" // something like this?
    }

    let contentIDs: [UUID]
}

Upvotes: 16

Views: 9056

Answers (5)

Sandeep Chhabra
Sandeep Chhabra

Reputation: 61

A better and more cleaner solution could be to create an extension on KeyedDecodingContainer which can be reused.

    extension KeyedDecodingContainer{
        enum ParsingError:Error{
          case noKeyFound
          /*
            Add other errors here for more use cases
          */
        }

        func decode<T>(_ type:T.Type, forKeys keys:[K]) throws -> T where T:Decodable {
           for key in keys{
             if let val = try? self.decode(type, forKey: key){
               return val
             }
           }
         throw ParsingError.noKeyFound
       }
    }
    

Above function can be used as follows:

struct Foo:Decodable{
     let name:String
     let contentIDs: [String]

     enum CodingKeys:String,CodingKey {
         case name
         case contentIds, Ids, IDs
     }

    init(from decoder:Decoder) throws{
        let container = try decoder.container(keyedBy:CodingKeys.self)
        contentIDs = try container.decode([String].self, forKeys:[.contentIds,.IDs,.Ids])
        name = try container.decode(String.self, forKey: .name)
    }
}

Upvotes: 6

Gurkan Soykan
Gurkan Soykan

Reputation: 196

You can do that by using multiple CodingKey enums and custom initializer. Let me show it by an example

enum CodingKeys: String, CodingKey {
    case name = "prettyName"
}

enum AnotherCodingKeys: String, CodingKey {
    case name
}

init(from decoder: Decoder) throws {
    let condition = true // or false
    if condition {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
    } else {
        let values = try decoder.container(keyedBy: AnotherCodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
    }
}

Upvotes: 13

Rob Napier
Rob Napier

Reputation: 299275

You can't do literally what you're describing, but you can make the process quite mechanical, and you can turn this into autogenerated code using Sourcery if you need that.

First, as usual, you need an AnyKey (someday I hope this is added to stdlib; even the Apple docs reference it....)

struct AnyKey: CodingKey {
    var stringValue: String
    var intValue: Int?
    init(stringValue: String) {
        self.stringValue = stringValue
    }

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

Then you want a new method that can decode from a list of possible keys. This particular implementation tries elements in the dictionary, and then falls back to the name of the key. (This way you don't have to put thing in the dictionary if they only have their own name.)

extension KeyedDecodingContainer where K == AnyKey {
    func decode<T>(_ type: T.Type, forMappedKey key: String, in keyMap: [String: [String]]) throws -> T where T : Decodable{

        for key in keyMap[key] ?? [] {
            if let value = try? decode(T.self, forKey: AnyKey(stringValue: key)) { 
                return value
            }
        }

        return try decode(T.self, forKey: AnyKey(stringValue: key))
    }
}

And finally, the tedious but simple (and code-generated if you like) init:

init(from decoder: Decoder) throws {
    let keyMap = [
        "contentIDs": ["contentIds", "Ids", "IDs"],
        "title": ["name"],
    ]

    let container = try decoder.container(keyedBy: AnyKey.self)

    self.contentIDs = try container.decode([UUID].self, forMappedKey: "contentIDs", in: keyMap)
    self.title = try container.decode(String.self, forMappedKey: "title", in: keyMap)
    self.count = try container.decode(Int.self, forMappedKey: "count", in: keyMap)
}

You can make it even tidier with a local function:

init(from decoder: Decoder) throws {
    let keyMap = [
        "contentIDs": ["contentIds", "Ids", "IDs"],
        "title": ["name"],
    ]

    let container = try decoder.container(keyedBy: AnyKey.self)

    func decode<Value>(_ key: String) throws -> Value where Value: Decodable {
        return try container.decode(Value.self, forMappedKey: key, in: keyMap)
    }

    self.contentIDs = try decode("contentIDs")
    self.title = try decode("title")
    self.count = try decode("count")
    // ...
}

I don't think you can get much simpler than this using Decodable, however, because you can't decode dynamic types and Swift needs to be certain you initialize all the properties. (These make it very hard to create a for loop to do initialization.)

Upvotes: 5

matt
matt

Reputation: 534925

You can't do what you're asking to do. From the question and your later comments, it appears you've got some very bad JSON. Decodable is not made for that sort of thing. Use JSONSerialization and clean up the mess afterward.

Upvotes: 0

PGDev
PGDev

Reputation: 24341

Implement the init(from:) initialiser and add custom parsing as per your requirement, i.e.

struct Foo: Decodable {
    let contentIDs: [String]

    enum CodingKeys: String, CodingKey, CaseIterable {
        case contentIds, Ids, IDs
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let key = container.allKeys.filter({ CodingKeys.allCases.contains($0) }).first, let ids = try container.decodeIfPresent([String].self, forKey: key) {
            self.contentIDs = ids
        } else {
            self.contentIDs = []
        }
    }
}

Upvotes: 6

Related Questions