0

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?

2
  • 1
    Please post proper json so we can use it directly without having to correct it first Commented Feb 16, 2022 at 22:28
  • Sorry @JoakimDanielson, I was typing an "example" and missed the double-quotes around several objects. Should have run it through JSON Lint. Apologies. Commented Feb 17, 2022 at 0:29

2 Answers 2

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
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

I am kind of a newbie so be kind ;-) I understand what the enum is doing but don't know how to call the initializer. The TaxHistoryItem (your example) is part of more than 50 keys in one object and TaxHistoryItem is one of 34 of those 50+ keys. I hope that made sense. Very much appreciative of the help!
In your MlsItemData replace the line let tax_history: [MlsItemTaxHistory]? with let tax_history: TaxHistory and decode as you have been normally.
0

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
}

2 Comments

Thanks for that solution but it isn't quite what I was after. Using your example the TaxHistoryList contains about 53 keys involving 34 objects which includes the TaxHistory object. Your solution means I have to manually decode all of them right? Seriously, thanks for your help!
No this is the whole point of this solution, you don’t need to implement another init(from:) and manually decode a lot of stuff, I suspected you had a lot of other attributes so that is why I am suggesting this. What you do is that you declare tax_history to be of this type, let tax_history: TaxHistoryList and that’s all.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.