使用具有多个键的 Codable 协议

Posted

技术标签:

【中文标题】使用具有多个键的 Codable 协议【英文标题】:Using Decodable protocol with multiples keys 【发布时间】:2017-10-13 06:17:55 【问题描述】:

假设我有以下代码:

import Foundation

let jsonData = """
[
    "firstname": "Tom", "lastname": "Smith", "age": "realage": "28",
    "firstname": "Bob", "lastname": "Smith", "age": "fakeage": "31"
]
""".data(using: .utf8)!

struct Person: Codable 
    let firstName, lastName: String
    let age: String?

    enum CodingKeys : String, CodingKey 
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    


let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
print(decoded)

除了age 始终为nil 之外,一切正常。这是有道理的。我的问题是如何在第一个示例中设置 Person 的年龄 = realage28,在第二个示例中设置 nil。而不是age 在这两种情况下都是nil,我希望它在第一种情况下是28

有没有办法只使用CodingKeys 而不必添加另一个结构或类来实现这一点?如果不是,我如何使用另一个结构或类以最简单的方式实现我想要的?

【问题讨论】:

你可以在里面写,自己编码。在那里你可以要求嵌套的年龄容器并相应地设置值。 @HAS 所以我刚刚在Encode and Decode ManuallyAdditionalInfoKeys 上找到了this。但是我收到一个错误Designated initializer cannot be declared in an extension of 'Person'; did you mean this to be a convenience initializer?Initializer requirement 'init(from:)' can only be satisfied by a 'required' initializer in the definition of non-final class 'Person' 是的,你不能在扩展中这样做,你必须把它写在你声明类型的地方。 【参考方案1】:

在解码嵌套的 JSON 数据时,我最喜欢的方法是定义一个与 JSON 非常接近的“原始”模型,甚至在需要时使用 snake_case。它有助于将 JSON 数据快速导入 Swift,然后您可以使用 Swift 进行所需的操作:

struct Person: Decodable 
    let firstName, lastName: String
    let age: String?

    // This matches the keys in the JSON so we don't have to write custom CodingKeys    
    private struct RawPerson: Decodable 
        struct RawAge: Decodable 
            let realage: String?
            let fakeage: String?
        

        let firstname: String
        let lastname: String
        let age: RawAge
    

    init(from decoder: Decoder) throws 
        let rawPerson  = try RawPerson(from: decoder)
        self.firstName = rawPerson.firstname
        self.lastName  = rawPerson.lastname
        self.age       = rawPerson.age.realage
    

另外,我建议您谨慎使用Codable,因为它同时暗示了EncodableDecodable。看来您只需要Decodable,因此您的模型仅符合该协议。

【讨论】:

【参考方案2】:

为了获得更大的灵活性和稳健性,您可以实现Age 枚举以完全支持您的数据模型;)例如:

enum Age: Decodable 
    case realAge(String)
    case fakeAge(String)

    private enum CodingKeys: String, CodingKey 
        case realAge = "realage", fakeAge = "fakeage"
    

    init(from decoder: Decoder) throws 
        let dict = try decoder.container(keyedBy: CodingKeys.self)
        if let age = try dict.decodeIfPresent(String.self, forKey: .realAge) 
            self = .realAge(age)
            return
        
        if let age = try dict.decodeIfPresent(String.self, forKey: .fakeAge) 
            self = .fakeAge(age)
            return
        
        let errorContext = DecodingError.Context(
            codingPath: dict.codingPath,
            debugDescription: "Age decoding failed"
        )
        throw DecodingError.keyNotFound(CodingKeys.realAge, errorContext)
    

然后在您的Person 类型中使用它:

struct Person: Decodable 
    let firstName, lastName: String
    let age: Age

    enum CodingKeys: String, CodingKey 
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    

    var realAge: String? 
        switch age 
        case .realAge(let age): return age
        case .fakeAge: return nil
        
    

像以前一样解码:

let jsonData = """
[
    "firstname": "Tom", "lastname": "Smith", "age": "realage": "28",
    "firstname": "Bob", "lastname": "Smith", "age": "fakeage": "31"
]
""".data(using: .utf8)!

let decoded = try! JSONDecoder().decode([Person].self, from: jsonData)
for person in decoded  print(person) 

打印:

人(名字:“汤姆”,姓氏:“史密斯”,年龄:Age.realAge(“28”)) Person(firstName: "Bob", lastName: "Smith", age: Age.fakeAge("31"))


最后,新的realAge 计算属性提供了您最初所追求的行为(即,非零适用于实际年龄):

for person in decoded  print(person.firstName, person.realAge) 

汤姆可选(“28”) 鲍勃无

【讨论】:

【参考方案3】:

有时会欺骗 API 以获取您想要的接口。

let jsonData = """
[
    "firstname": "Tom", "lastname": "Smith", "age": "realage": "28",
    "firstname": "Bob", "lastname": "Smith", "age": "fakeage": "31"
]
""".data(using: .utf8)!

struct Person: Codable 
    let firstName: String
    let lastName: String
    var age: String?  return _age["realage"] 

    enum CodingKeys: String, CodingKey 
        case firstName = "firstname"
        case lastName = "lastname"
        case _age = "age"
    

    private let _age: [String: String]


do 
    let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
    print(decoded)

    let encoded = try JSONEncoder().encode(decoded)
    if let encoded = String(data: encoded, encoding: .utf8)  print(encoded) 
 catch 
    print(error)

这里保留了 API(firstNamelastNameage),并且双向保留了 JSON。

【讨论】:

【参考方案4】:

你可以这样使用:

struct Person: Decodable 
    let firstName, lastName: String
    var age: Age?

    enum CodingKeys: String, CodingKey 
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    


struct Age: Decodable 
    let realage: String?

你可以这样调用:

do 
    let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
    print(decoded[0].age?.realage) // Optional("28")
    print(decoded[1].age?.realage) // nil
 catch 
    print("error")

【讨论】:

【参考方案5】:

这里有很多很棒的答案。我有某些理由不想将它变成它自己的数据模型。特别是在我的情况下,它带有很多我不需要的数据,而我需要的这个特定的东西更多地对应于一个人而不是一个年龄模型。

我相信其他人会发现这篇文章很有用,这太棒了。除此之外,我将发布我决定如何执行此操作的解决方案。

查看Encoding and Decoding Custom Types Apple Documentation 后,我发现可以构建自定义解码器和编码器来实现这一点(手动编码和解码)。

struct Coordinate: Codable 
    var latitude: Double
    var longitude: Double
    var elevation: Double

    enum CodingKeys: String, CodingKey 
        case latitude
        case longitude
        case additionalInfo
    

    enum AdditionalInfoKeys: String, CodingKey 
        case elevation
    

    init(from decoder: Decoder) throws 
        let values = try decoder.container(keyedBy: CodingKeys.self)
        latitude = try values.decode(Double.self, forKey: .latitude)
        longitude = try values.decode(Double.self, forKey: .longitude)

        let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
    

    func encode(to encoder: Encoder) throws 
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(latitude, forKey: .latitude)
        try container.encode(longitude, forKey: .longitude)

        var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        try additionalInfo.encode(elevation, forKey: .elevation)
    


Apple 未提及的上述代码中包含的一项更改是,您不能像在他们的文档示例中那样使用扩展。所以你必须将它嵌入到结构或类中。

希望这对某人有所帮助,以及此处的其他惊人答案。

【讨论】:

...所有这些好的答案只是为了展示这个 Swift 4 编码/解码的东西真的是多么强大/灵活;) @PauloMattos 完全是,只是希望有更多的文档和信息,哈哈。所以希望这个 Stack Overflow 问题将有助于创建它并帮助其他人。 @PauloMattos 在this one 上使用我的回答以上的任何想法?

以上是关于使用具有多个键的 Codable 协议的主要内容,如果未能解决你的问题,请参考以下文章

如何从具有多个值的 Codable 结构中获取一个值?

带有动态键的 Swift Codable

如何使用 `Codable` 协议调用`didSet` 方法

Swift 使用Codable协议进行json转模型

Codable和CodingKeys

Codable : 不符合协议'Decodable'