使用包含不同类型字典的 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 是一个字典数组。 homework 每个都包含一个地址,而 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]

Responseaddresses属性修改为[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

Swift模型中数组和字典的JSON解码

Swift 4 JSON Parsing(预期解码 Array<Any> 但找到了字典)

类型不匹配 Swift JSON

使用 Alamofire 将 JSON 解码为对象

在 Swift 中解码 JSON API - 重复的结构名称