Home > Software design >  Specific JSON parsing issue - array of varying objects - Swift
Specific JSON parsing issue - array of varying objects - Swift

Time:09-21

I have a block of JSON I have to consume. I have no control over the shape of the JSON data.

Let's say I have a response blob that looks like this:

{
"resultStatus" : 1,
"resultEntities" : [...]
}

Inside the resultEntities array, there are two distinct objects; one type is always at index 0, essentially a header for everything that follow, and indices 1...-> contain another type (I can control the type I ask for). There's some overlap of fields between the two objects, but only a couple of fields out of a total of about 30 fields.

{
"rectype" : 1,
"recname" : "header",
"companyname" : "Smithson & Jones",
"companyId" : "q1w2e3r4",
...
}

and

{
"rectype" : 2,
"recname" : "detail record",
"locationId" : "123 Miami Warehouse",
"shelvingUnits" : 654,
...
}

My receiving object looks basically like this:

struct APIResponse : Decodable {
let resultStatus : Int
let results : [...] //<--- and there is the issue

I don't think I can define my receiving object so that results[0] always tries to parse to header, and all other ones parse to details, right?

I obviously can't do something like this (pseudocode, I know this won't compile - it's just here to clarify what I am dealing with):

let results : [ 0 = header type, ... = detail type ]

or

let results[0] : Header 
let results[...] : Detail

and so forth.

So, should the object that is the array in results just be an amalgamation of header and detail with all fields (except known overlappers) being optional?

I hope I'm explaining this well enough.

Thoughts? (happy to answer any questions to narrow in on the details if needed, and update the question accordingly)

CodePudding user response:

Here is a solution using only Codable, with a custom init(from:) where we use an unkeyed container for decoding the content of the array. Note that the header value has been moved out of the array and into its own property in the Result struct

struct Result: Decodable {
  let status: Int
  let header: Header
  var entities: [DetailRecord]
  
  enum CodingKeys: String, CodingKey {
    case status = "resultStatus"
    case entities = "resultEntities"
  }
  
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    status = try container.decode(Int.self, forKey: .status)
    var nested = try container.nestedUnkeyedContainer(forKey: .entities)
    header = try nested.decode(Header.self)
    
    entities = []
    while !nested.isAtEnd {
      let detail = try nested.decode(DetailRecord.self)
      entities.append(detail)
    }
  }
}

If you just want a quick-and-dirty solution you can use JSONSerialzation

do {
  let dictionary = try JSONSerialization.jsonObject(with: data) as! [String: Any]
  
  if let entities = dictionary["resultEntities"] as? [Any], !entities.isEmpty {
    let header = entities.first!
      print(header)
    
    let details = Array(entities.dropFirst())
    print(details)
  }
} catch {
  print(error)
}

Of course this will be more cumbersome when accessing individual properties compared to having predefined types that conform to Codable.

One way to use Codable here is to first convert the header and details variables to json data object and then use a decoder to convert them to proper objects

let detailsData = try JSONSerialization.data(withJSONObject: details)
let headerData = try JSONSerialization.data(withJSONObject: header)

let decoder = JSONDecoder()
let headerObject = try decoder.decode(Header.self, from: headerData)
let detailsArray = try decoder.decode([DetailRecord].self, from: detailsData)
  • Related