Home > Software design >  JSON decoder for Swift dealing with changing underlying JSON with Array and Dictionary
JSON decoder for Swift dealing with changing underlying JSON with Array and Dictionary

Time:02-17

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?

CodePudding user response:

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

CodePudding user response:

You could create a specific type that holds this array and then write a custom init(from:) for it.

In the init we try to decode the json as an array and if it fails we simply assign an empty array to the property (nil for an optional property is another possible solution but I prefer an empty collection before 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
}
  • Related