Search code examples
jsonswiftjsondecoder

Decoding an array containing multiple types


Hello does anyone know a nice way to decode the below Json? It is array with different elements in it. I tried to decode it using keyedBy for the container but I could not make it work.

For the json example, the first element is a Journey object and an array of Bookings, the object 2 and 3 are just plain bookings.

{
    "archives": [
      {
        "journey": {
          "id": 5,
          "name": "test name"
        },
        "bookings": [
          {
            "id": 563219,
            "address": "test address"         
          },
          {
            "id": 563220,
            "address": "test address 2"   
          }
        ]
      },
      {
          "id": 563221,
          "address": "test address 3"  
      },
      {
          "id": 563222,
          "address": "test address 4"  
      }
    ]
  }

This is what I tried so far but it does not work. It goes 3 times into the Job init because the array has 3 object but It does not know how to decode because the coding keys does not match.

let jobs = try? JSONDecoder().decode(Response.self, from: jsonData)
struct Response: Decodable {
    let response: Archive
}

struct Archive: Decodable {
    let archives: [Job]
}

struct Booking: Decodable {
    let id: Int
    let address: String
}

struct JourneyDetail: Decodable {
    let journey: Journey?
    let bookings: [Booking]?
}

struct Journey: Decodable {
    let id: Int
    let name: String
}


enum Job: Decodable {
    case journeyDetail(JourneyDetail)
    case booking(Booking)

    enum CodingKeys: CodingKey, CaseIterable {
        case journeyDetail
        case booking
    }

    init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try container.decodeIfPresent(JourneyDetail.self, forKey: .journeyDetail) {
            self = Job.journeyDetail(value)
            return
        }

        if let value = try container.decodeIfPresent(Booking.self, forKey: .booking) {
            self = Job.booking(value)
            return
        }

        throw DecodingError.valueNotFound(Self.self, DecodingError.Context(codingPath: CodingKeys.allCases, debugDescription: "objects not found"))
    }
}

Solution

  • Your top level type seems to be irrelevant and I also changed the naming somewhat so here are the structures I used

    struct Response: Decodable {
        let archives: [Archive]
    }
    
    enum Archive: Decodable {
        case journeyDetail(JourneyDetail)
        case booking(Booking)
    }
    
    struct JourneyDetail: Decodable {
        let journey: Journey?
        let bookings: [Booking]
    }
    
    struct Journey: Decodable {
        let id: Int
        let name: String
    }
    
    struct Booking: Decodable {
        let id: Int
        let address: String
    }
    

    What is needed then is to manually decode the top level array archives so we need coding keys and a custom init in Response

    enum CodingKeys: String, CodingKey {
        case archives
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        var nestedContainer = try container.nestedUnkeyedContainer(forKey: .archives)
        var temp = [Archive]()
        while nestedContainer.isAtEnd == false {
            if let journey = try? nestedContainer.decode(JourneyDetail.self) {
                temp.append(.journeyDetail(journey))
            } else {
                let booking = try nestedContainer.decode(Booking.self)
                temp.append(.booking(booking))
            }
        }
        archives = temp
    }
    

    This will give you an array of the enum but another option that might be suitable is to get an array of the struct JourneyDetail instead.

    Here I removed the enum and changed Response as can be seen below but all other types are the same

    struct Response: Decodable {
        let archives: [JourneyDetail]
    
        enum CodingKeys: String, CodingKey {
            case archives
        }
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            var nestedContainer = try container.nestedUnkeyedContainer(forKey: .archives)
            var temp = [JourneyDetail]()
            while nestedContainer.isAtEnd == false {
                if let archive = try? nestedContainer.decode(JourneyDetail.self) {
                    temp.append(archive)
                } else {
                    let booking = try nestedContainer.decode(Booking.self)
                    temp.append(JourneyDetail(journey: nil, bookings: [booking]))
                }
            }
            archives = temp
        }
    }