【问题标题】:Decoding JSON with Swift containing dictionary with different types使用包含不同类型字典的 Swift 解码 JSON
【发布时间】: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 我不知道存在这样的工具。我用谷歌搜索了一下,发现了一些不错的。最终我最终使用了一个,因为它与我的代码相比我在下面选择的答案更好,所以谢谢。

标签: json swift codable


【解决方案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
}()

【讨论】:

  • 感谢您的回复!这是另一个好方法。对于我的特殊情况,我选择了另一种方法,但我也喜欢这种方法,这是很好的信息,将来会对我有所帮助。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-28
  • 2021-10-30
  • 1970-01-01
  • 2018-06-27
  • 1970-01-01
  • 2020-04-20
相关资源
最近更新 更多