对单个属性使用多个CodingKeys

时间:2019-10-04 12:11:34

标签: swift

是否可以对一个属性使用多个CodingKeys?

struct Foo: Decodable {

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

    let contentIDs: [UUID]
}

5 个答案:

答案 0 :(得分:2)

实施 init(from:) 初始化程序,并根据您的要求添加自定义解析,即

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 = []
        }
    }
}

答案 1 :(得分:2)

您不能按字面意思进行操作,但是可以使过程非常机械化,并且可以根据需要使用Sourcery将其转换为自动生成的代码。

首先,与往常一样,您需要一个AnyKey(总有一天我希望将其添加到stdlib;甚至Apple文档都引用它。...)

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
    }
}

然后,您需要一种可以从可能的密钥列表中解码的新方法。这个特定的实现尝试使用字典中的元素,然后退回到键的名称。 (这样,如果它们只有自己的名称,则不必在字典中放入内容。)

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))
    }
}

最后,乏味但简单(如果愿意,可以生成代码):

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)
}

您可以使用本地函数使其更加整洁:

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")
    // ...
}

但是,我认为使用Decodable不会比这简单得多,因为您无法解码动态类型,而Swift需要确保初始化所有属性。 (这使得创建for循环进行初始化非常困难。)

答案 2 :(得分:1)

您不能做您想做的事。从问题和您以后的评论中,您似乎发现了一些非常糟糕的JSON。可腐烂不是为这种事情而做的。使用JSONSerialization并随后清理混乱。

答案 3 :(得分:1)

您可以通过使用多个CodingKey枚举和自定义初始化程序来实现。 让我通过示例展示

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)
    }
}

答案 4 :(得分:1)

一种更好,更清洁的解决方案是在KeyedDecodingContainer上创建一个扩展,该扩展可以重复使用。

    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
       }
    }
    

以上功能可以按如下方式使用:

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)
    }
}