假设我有以下代码:
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
或28
,在第二个示例中设置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
,因为它意味着Encodable
和Decodable
.似乎您只需要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(firstName
、lastName
、age
),并且双向保留了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 没有提到的更改是,您不能像他们的文档示例中那样使用扩展。因此,您必须将其嵌入到结构或类中。
希望这对某人有所帮助,以及这里的其他惊人答案。