使用具有多个键的 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 的年龄 = realage
或 28
,在第二个示例中设置 nil
。而不是age
在这两种情况下都是nil
,我希望它在第一种情况下是28
。
有没有办法只使用CodingKeys
而不必添加另一个结构或类来实现这一点?如果不是,我如何使用另一个结构或类以最简单的方式实现我想要的?
【问题讨论】:
你可以在里面写,自己编码。在那里你可以要求嵌套的年龄容器并相应地设置值。 @HAS 所以我刚刚在Encode and Decode Manually
和AdditionalInfoKeys
上找到了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
,因为它同时暗示了Encodable
和Decodable
。看来您只需要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(firstName
、lastName
、age
),并且双向保留了 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 协议的主要内容,如果未能解决你的问题,请参考以下文章