使用包含不同类型字典的 Swift 解码 JSON
Posted
技术标签:
【中文标题】使用包含不同类型字典的 Swift 解码 JSON【英文标题】:Decoding JSON with Swift containing dictionary with different types 【发布时间】:2020-11-29 14:48:20 【问题描述】:我有一个 JSON 格式,我正在尝试使用 JSONDecoder 解析,但由于 JSON 的结构方式,我不知道该怎么做。
这是 JSON 的格式。为简洁起见,我将省略一些内容。
"name":"John Smith",
"addresses":[
"home":
"street":"123 Main St",
"state":"CA"
,
"work":
"street":"345 Oak St",
"state":"CA"
,
"favorites":[
"street":"456 Green St.",
"state":"CA"
,
"street":"987 Tambor Rd",
"state":"CA"
]
]
我不知道如何定义一个可以解码的 Decodable 结构。 addresses
是一个字典数组。 home
和 work
每个都包含一个地址,而 favorites
包含一个地址数组。我不能将地址定义为[Dictionary<String, Address]
,因为favorites
是一个地址数组。我无法将其定义为 [Dictionary<String, Any>]
,因为这样我会收到 Type 'UserProfile' does not conform to protocol 'Encodeable'
错误。
有没有人知道如何解析这个?如何解析值随键变化的字典?
谢谢。
【问题讨论】:
这不是一个有效的 JSON 字符串。您能否添加您从 API 获得的确切响应 使用在线工具将 Json 转换为可编码模型,只需 google 即可 我从 MongoDB 文档中复制了结构,所以这很可能是 JSON 不正确的原因。 @PratikPrajapati 我不知道存在这样的工具。我用谷歌搜索了一下,发现了一些不错的。最终我最终使用了一个,因为它与我的代码相比我在下面选择的答案更好,所以谢谢。 【参考方案1】:我假设你的 JSON 是这样的:
"name": "John Smith",
"addresses": [
"home":
"street": "123 Main St",
"state": "CA"
,
"work":
"street": "345 Oak St",
"state": "CA"
,
"favorites": [
"street": "456 Green St.",
"state": "CA"
,
"street": "987 Tambor Rd",
"state": "CA"
]
]
我必须进行一些更改才能成为有效的 JSON。
您可以使用以下结构始终将地址属性映射到[[String: [Address]]]
:
struct Response: Decodable
let name: String
let addresses: [[String: [Address]]]
enum CodingKeys: String, CodingKey
case name
case addresses
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .addresses)
var addresses = [[String: [Address]]]()
while !unkeyedContainer.isAtEnd
do
let sindleAddress = try unkeyedContainer.decode([String: Address].self)
addresses.append(sindleAddress.mapValues [$0] )
catch DecodingError.typeMismatch
addresses.append(try unkeyedContainer.decode([String: [Address]].self))
self.addresses = addresses
struct Address: Decodable
let street: String
let state: String
基本上,在init(from:)
的自定义实现中,我们尝试将addresses
属性解码为[String: Address]
,如果成功则创建一个[String: [Address]]
类型的新字典,其值数组中只有一个元素。如果失败,我们将addresses
属性解码为[String: [Address]]
。
更新:我更愿意添加另一个结构:
struct AddressType
let label: String
let addressList: [Address]
将Response
的addresses
属性修改为[AddressType]
:
struct Response: Decodable
let name: String
let addresses: [AddressType]
enum CodingKeys: String, CodingKey
case name
case addresses
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .addresses)
var addresses = [AddressType]()
while !unkeyedContainer.isAtEnd
let addressTypes: [AddressType]
do
addressTypes = try unkeyedContainer.decode([String: Address].self).map
AddressType(label: $0.key, addressList: [$0.value])
catch DecodingError.typeMismatch
addressTypes = try unkeyedContainer.decode([String: [Address]].self).map
AddressType(label: $0.key, addressList: $0.value)
addresses.append(contentsOf: addressTypes)
self.addresses = addresses
【讨论】:
谢谢!!我缺少的部分是循环遍历nestedUnkeyedContainer 中的条目。我不知道我能做到这一点,如果我做到了,我不知道我是否会考虑捕获一个解码错误来区分两种不同的类型。【参考方案2】:一个可能的解决方案,使用enum
,可以是工作、家庭或收藏:
struct Top: Decodable
let name: String
let addresses: [AddressType]
enum CodingKeys: String, CodingKey
case name
case addresses
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.addresses = try container.decode([AddressType].self, forKey: .addresses)
enum AddressType: Decodable
case home(Address)
case work(Address)
case favorites([Address])
enum CodingKeys: String, CodingKey
case home
case work
case favorites
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
if let home = try container.decodeIfPresent(Address.self, forKey: .home)
self = AddressType.home(home)
else if let work = try container.decodeIfPresent(Address.self, forKey: .work)
self = AddressType.work(work)
else
let favorites = try container.decodeIfPresent([Address].self, forKey: .favorites)
self = AddressType.favorites(favorites ?? [])
struct Address: Decodable
let street: String
let state: String
测试(我猜对你的 JSON 进行了修复):
let jsonStr = """
"name": "John Smith",
"addresses": [
"home":
"street": "123 Main St",
"state": "CA"
,
"work":
"street": "345 Oak St",
"state": "CA"
,
"favorites": [
"street": "456 Green St.",
"state": "CA"
,
"street": "987 Tambor Rd",
"state": "CA"
]
]
"""
let jsonData = jsonStr.data(using: .utf8)!
do
let top = try JSONDecoder().decode(Top.self, from: jsonData)
print("Top.name: \(top.name)")
top.addresses.forEach
switch $0
case .home(let address):
print("It's a home address:\n\t\(address)")
case .work(let address):
print("It's a work address:\n\t\(address)")
case .favorites(let addresses):
print("It's a favorites addresses:")
addresses.forEach aSubAddress in
print("\t\(aSubAddress)")
catch
print("Error: \(error)")
输出:
$>Top.name: John Smith
$>It's a home address:
Address(street: "123 Main St", state: "CA")
$>It's a work address:
Address(street: "345 Oak St", state: "CA")
$>It's a favorites addresses:
Address(street: "456 Green St.", state: "CA")
Address(street: "987 Tambor Rd", state: "CA")
注意:
之后,您应该可以根据需要在 Top
上添加惰性变量:
lazy var homeAddress: Address? =
return self.addresses.compactMap
if case AddressType.home(let address) = $0
return address
return nil
.first
()
【讨论】:
感谢您的回复!这是另一个好方法。对于我的特殊情况,我选择了另一种方法,但我也喜欢这种方法,这是很好的信息,将来会对我有所帮助。谢谢!以上是关于使用包含不同类型字典的 Swift 解码 JSON的主要内容,如果未能解决你的问题,请参考以下文章
使用混合类型和混合键控/非键控在 Swift 中解码 JSON