Home > OS >  Decode a nested object in Swift 5 with custom initializer
Decode a nested object in Swift 5 with custom initializer

Time:01-02

I have an API which returns a payload like this (just one item is included in the example).

{
  "length": 1,
  "maxPageLimit": 2500,
  "totalRecords": 1,
  "data": [
    {
      "date": "2021-05-28",
      "peopleCount": 412
    }
  ]
}

I know I can actually create a struct like

struct Root: Decodable {
  let data: [DailyCount]
}

struct DailyCount: Decodable {
  let date: String
  let peopleCount: Int
}

For different calls, the same API returns the same format for the root, but the data is then different. Moreover, I do not need the root info (length, totalRecords, maxPageLimit). So, I am considering to create a custom init in struct DailyCount so that I can use it in my URL session

let reports = try! JSONDecoder().decode([DailyCount].self, from: data!)

Using Swift 5 I tried this:

struct DailyCount: Decodable {
  let date: String
  let peopleCount: Int
}

extension DailyCount {
  enum CodingKeys: String, CodingKey {
    case data
    enum DailyCountCodingKeys: String, CodingKey {
      case date
      case peopleCount
    }
  }
  init(from decoder: Decoder) throws {
    // This should let me access the `data` container
    let container = try decoder.container(keyedBy: CodingKeys.self
    peopleCount = try container.decode(Int.self, forKey: . peopleCount)
    date = try container.decode(String.self, forKey: .date)
  }
}

Unfortunately, it does not work. I get two problems:

  1. The struct seems not to conform anymore to the Decodable protocol
  2. The CodingKeys does not contain the peopleCount (therefore returns an error)

CodePudding user response:

This can’t work for multiple reasons. You are trying to decode an array, so your custom decoding implementation from DailyCount won’t be called at all (if it were to compile) since at the top level your JSON contains an object, not an array.

But there is a much simpler solution which doesn’t even require implementing Decodable yourself.

You can create a generic wrapper struct for your outer object and use that with whatever payload type you need:

 struct Wrapper<Payload: Decodable>: Decodable {
     var data: Payload
 }

You then can use this to decode your array of DailyCount structs:

let reports = try JSONDecoder().decode(Wrapper<[DailyCount]>.self, from: data).data

This can be made even more transparent by creating an extension on JSONDecoder:

extension JSONDecoder {
     func decode<T: Decodable>(payload: T.Type, from data: Data) throws -> T {
          try decode(Wrapper<T>.self, from: data).data
     }
}

CodePudding user response:

Sven's answer is pure and elegant, but I would be remiss if I didn't point out that there is also a stupid but easy way: stop overthinking this and just dumpster-dive into the "data" without using Codable at all. Example:

// preconditions
let json = """
{
  "length": 1,
  "maxPageLimit": 2500,
  "totalRecords": 1,
  "data": [
    {
      "date": "2021-05-28",
      "peopleCount": 412
    }
  ]
}
"""
let jsonData = json.data(using: .utf8)!
struct DailyCount: Decodable {
    let date: String
    let peopleCount: Int
}

// okay, here we go
do {
    let dict = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [AnyHashable:Any]
    let arr = dict?["data"] as? Array<Any>
    let json2 = try JSONSerialization.data(withJSONObject: arr as Any, options: [])
    let output = try JSONDecoder().decode([DailyCount].self, from: json2)
    print(output) // yep, it's an Array of DailyCount
} catch {
    print(error)
}
  • Related