用 Swift 解码 JSON 包含不同类型的字典

Decoding JSON with Swift containing dictionary with different types

我有一个 JSON 格式,我试图用 JSON 解码器解析,但由于 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' 错误。

有人知道我该如何解析吗?如何解析值根据键而变化的字典?

谢谢。

一个可能的解决方案,使用 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 [=11=] {
        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) = [=13=] {
            return address
        }
        return nil
    }.first
}()

我假设你的 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 { [[=11=]] } )
            } 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],如果成功,则类型为 [=18= 的新字典] 是用值数组中的一个元素创建的。如果失败,那么我们将 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: [=13=].key, addressList: [[=13=].value])
                }
            } catch DecodingError.typeMismatch {
                addressTypes = try unkeyedContainer.decode([String: [Address]].self).map {
                    AddressType(label: [=13=].key, addressList: [=13=].value)
                }
            }
            addresses.append(contentsOf: addressTypes)
        }
        self.addresses = addresses
    }
}