使用具有多个密钥的可解码协议



假设我有以下代码:

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.这是有道理的。我的问题是如何在第一个示例中设置人员的年龄 = realage28,在第二个示例中设置nil。与其age在这两种情况下都nil,我希望在第一种情况下28

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

解码嵌套的 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因此仅使您的模型符合该协议。

为了获得更大的灵活性和健壮性,您可以实现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) }

指纹:

人(名字:"汤姆",姓氏:"史密斯",年龄:年龄.realAge("28"))
人(名字:"鲍勃",姓氏:"史密斯",年龄:年龄.假年龄("31"))


最后,新的 realAge 计算属性提供了您最初追求的行为(即,针对实际年龄的非 nil):

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

汤姆可选("28")
鲍勃·

有时会欺骗 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。

你可以

这样使用:

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

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

我相信其他人会发现这篇文章很有用,这真是太棒了。补充一点,我将发布我如何决定这样做的解决方案。

在查看了 编码和解码自定义类型 Apple 文档后,我发现可以构建自定义解码器和编码器来实现这一点(手动编码和解码)。

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 没有提到的更改是,您不能像他们的文档示例中那样使用扩展。因此,您必须将其嵌入到结构或类中。

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

相关内容

  • 没有找到相关文章

最新更新