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

I am using a third-party API to get data. It is a rather complex payload but I'm experiencing a problem with one return. For this example I'm over-simplifying the structure. This structure actually has 53 entries, 34 of which are structures themselves.

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?

There are a whole load of other data objects in this API's results but I'm focusing on one item with the label tax_history. When there is data to be shared the key contains an Array like below.

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

When the API has no data to share I was expecting:

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

or just not in the payload at all. But instead the API is sending:

"tax_history": { }

I'm having difficulty as to how to deal with this in the decoder. Obviously, the built in decoder returns the "Expected to decode Array but found a dictionary instead", but is there a simple way to write a custom decoder for "just" the tax_history key and how would it be written for either getting an Array or an empty dictionary?



Solution 1:[1]

Yes, it is possible to decode this unusual payload using JSONDecoder. One way to do so is to use a custom type to represent either the empty or non-empty scenarios, and implement a custom initializer function and attempt to decode both cases to see which one works:

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
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 dalton_c