JSON Swift 的解码器处理改变基础 JSON 与数组和字典

JSON decoder for Swift dealing with changing underlying JSON with Array and Dictionary

我正在使用第三方 API 来获取数据。这是一个相当复杂的有效载荷,但我遇到了一个 return 的问题。对于这个例子,我过度简化了结构。这个结构实际上有53个条目,其中34个是结构本身。

struct MlsItemData: Codable, Hashable {
   let mls_id: String
   let photos: [MlsItemPhoto]?
   let features: [MlsItemFeature]?
   let address: MlsItemAddress
   let move_in_date: String?
   let stories: Int?
   let client_flags: MlsItemClientFlags?
   let tax_history: [MlsItemTaxHistory]?    <-- our propblem child
   let new_construction: Bool?
   let primary: Bool?
   let prop_common: MlsItemPropertyCommon?

此 API 的结果中包含大量其他数据对象,但我关注的是带有标签 tax_history 的一项。当有数据要共享时,密钥包含如下所示的数组。

{
   "tax_history": [
      {
         "assessment": {
            "building": null,
            "total": 3900,
            "land": null
         },
         "tax": 683,
         "year": "2020"
      },
      {
         "assessment": {
            "building": null,
            "total": 4093,
            "land": null
         },
         "tax": 698,
         "year": 2019
      }
   ]
}

当 API 没有数据共享时,我期待:

"tax_history": [ ]
or
"tax_history": null

或者根本不在有效负载中。但是 API 正在发送:

"tax_history": { }

我不知道如何在解码器中处理这个问题。显然,内置解码器 return 是“预期解码数组但发现了字典”,但是有没有一种简单的方法可以为“仅” tax_history 键编写自定义解码器以及如何它是为获取数组还是空字典而编写的?

是的,可以使用 JSONDecoder 解码这个不寻常的负载。一种方法是使用自定义类型来表示空或 non-empty 场景,并实现自定义初始化函数并尝试解码这两种情况以查看哪种情况有效:

struct TaxHistoryItem: Decodable {
    let year: String
    // ...
}

enum TaxHistory: Decodable {
    case empty
    case items([TaxHistoryItem])

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let items = try? container.decode([TaxHistoryItem].self) {
            self = .items(items)
        } else {
            struct EmptyObject: Decodable {}
            // Ignore the result. We just want to verify that the empty object exists
            // and doesn't throw an error here.
            try container.decode(EmptyObject.self)
            self = .empty
        }
    }
}

您可以创建一个包含此数组的特定类型,然后为其编写自定义 init(from:)

在 init 中,我们尝试将 json 解码为数组,如果失败,我们只需将一个空数组分配给 属性(可选的 属性 为 nil 是另一个可能的解决方案,但我更喜欢 nil)

之前的空集合
struct TaxHistoryList: Codable {
    let history: [TaxHistory]

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let list = try? container.decode([TaxHistory].self) {
            history = list
        } else {
            history = []
        }
    }
}

struct TaxHistory: Codable {
    let tax: Int
    let year: String
    // other stuff
}