1

假设我有以下代码:

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。这是有道理的。我的问题是如何在第一个示例和第二个示例中设置人的年龄 =realage或。而不是在这两种情况下,我希望它在第一种情况下。28nilagenil28

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

4

5 回答 5

5

在解码嵌套 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要使您的模型符合该协议即可。

于 2017-10-14T01:23:30.453 回答
3

为了获得更大的灵活性和健壮性,您可以实施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”))
人(名字:“鲍勃”,姓氏:“史密斯”,年龄:Age.fakeAge(“31”))


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

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

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

于 2017-10-13T13:58:30.707 回答
2

有时会欺骗 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 ( firstName, lastName, age) 并在两个方向上保留 JSON。

于 2017-10-13T14:56:21.393 回答
1

你可以这样使用:

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")
}
于 2017-10-13T19:10:05.500 回答
1

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

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

在查看了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 未提及的上述代码中包含的一项更改是您不能像他们的文档示例中那样使用扩展。所以你必须将它嵌入到结构或类中。

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

于 2017-10-13T20:43:53.123 回答