0
votes

Please can someone help me point out what am doing wrong. I have searched other similar questions like this but non of it seems to help resolve this issue I am trying to decode json using Swift Codable

I have created nested codable struct, but it keeps failing.

The json am trying to decode can be found here - https://pastebin.com/C0QCREEF

This is the codable struct that is failing -

struct OutboundFlightData: Codable {
  var data: OutboundFlight
}
struct OutboundFlight: Codable {
  var outboundFlights: [OutboundFlightDetails]
}
struct OutboundFlightDetails: Codable {
  var amount: String
  var fareFamily: Bool
  var cabin: String
  var fareBasis: String
  var duration: String
  var itinerary: [OutboundFlightItinerary]
}
struct OutboundFlightItinerary: Codable {
  var timeOfDeparture: String
  var timeOfArrival: String
  var dateOfDeparture: String
  var dateOfArrival: String
  var departureLocation: String
  var departureDetails: FlightItineraryDetails
  var arrivalLocation: String
  var arrivalDetails: [FlightItineraryDetails]
  var departureTerminal: String?
  var arrivalTerminal: String?
  var operatingCompany: String
  var operatingDetails: FlightItineraryOperatingDetails
  var marketingCompany: String
  var marketingDetails: FlightItineraryOperatingDetails
  var flightNumber: String
 var electronicTicketing: String
 }
 struct FlightItineraryOperatingDetails: Codable {
   var iata: String
   var fullName: String
   var countryName: String
   var logoSmall: String
   var logo: String
  enum CodingKeys: String, CodingKey {
   case iata
   case fullName = "full_name"
   case logoSmall = "logo_small"
   case logo
   case countryName = "country_name"
  }
 }
 struct FlightItineraryDetails: Codable {
   var iata: String
   var name: String
   var cityName: String
   var stateName: String
   var countryName: String
  enum CodingKeys: String, CodingKey {
   case iata
   case name
   case cityName = "city_name"
   case stateName = "state_name"
   case countryName = "country_name"
 }
}

I expect the json to decode successfully. However I keep getting an error -

typeMismatch(
    Swift.Array<Any>,
    Swift.DecodingError.Context(
        codingPath: [
            CodingKeys(stringValue: "data", intValue: nil),
            CodingKeys(stringValue: "outboundFlights", intValue: nil),
              _JSONKey(stringValue: "Index 0", intValue: 0),
            CodingKeys(stringValue: "itinerary", intValue: nil),
              _JSONKey(stringValue: "Index 0", intValue: 0),
            CodingKeys(stringValue: "arrivalDetails", intValue: nil)
        ],
        debugDescription: "Expected to decode Array<Any> but found a dictionary instead.",
        underlyingError: nil
    )
)

But then when I change arrivalDetails from array. I get this error

typeMismatch(
    Swift.Dictionary<Swift.String, Any>,
    Swift.DecodingError.Context(
        codingPath: [
            CodingKeys(stringValue: "data", intValue: nil),
            CodingKeys(stringValue: "outboundFlights", intValue: nil), 
              _JSONKey(stringValue: "Index 6", intValue: 6),
            CodingKeys(stringValue: "itinerary", intValue: nil),
              _JSONKey(stringValue: "Index 1", intValue: 1),
            CodingKeys(stringValue: "arrivalDetails", intValue: nil)
        ],
        debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.",
        underlyingError: nil
    )
)
1
What is your understanding of those errors thus far? - Alexander
@Alexander my understanding at first is that arrivalDetails needs to be an array of FlightItineraryDetails. But after I changing it to array of FlightItineraryDetails and then got a different error message, frankly I became confused. Please can you point in the direction to fix this - Perfect Aduh
You're correct. The first error says that json["data"][0]["outboundFlights"][0]["itinerary"][0]["arrivalDetails] is an array but the second one says json["data"]["outboundFlights"][6]["itinerary"][1]["arrivalDetails] should be a dictionary. Notice the difference in the key paths. You should inspect the test case you're working with, and see what's going on. - Alexander
What's particularly strange is that in the second error, OutboundFlightData.data is subscripted by "outboundFlights" directly, which shouldn't be possible because it's an array. - Alexander
@Alexander thanks for the feedback. But Frankly am lost on how to fix this issue. Could you help some more please? - Perfect Aduh

1 Answers

1
votes

It looks like from the sample JSON you have provided, the arrivalDetails field is being filled either with a dictionary, or an empty array. That's the reason why when you switch between [FlightItineraryDetails] and FlightItineraryDetails, you get errors either way, because the decoder cannot parse a dictionary into an array, or an array into a dictionary.

This is unfortunately a case that Codable doesn't handle out of the box. You would need to make an enum for that case that tries to parse that key both ways, returning whichever one it found in the data.

(This code was shamelessly extracted from the output of feeding your JSON into https://app.quicktype.io. I always suggest using that for mapping JSON examples from APIs into Codable structures. It gets you at least 80% of the way there, and handles weird cases like this where APIs are not well-behaved.)

enum ArrivalDetails: Codable {
    case anythingArray([JSONAny])
    case details(Details)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode([JSONAny].self) {
            self = .anythingArray(x)
            return
        }
        if let x = try? container.decode(Details.self) {
            self = .details(x)
            return
        }
        throw DecodingError.typeMismatch(ArrivalDetails.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ArrivalDetails"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .anythingArray(let x):
            try container.encode(x)
        case .details(let x):
            try container.encode(x)
        }
    }
}

If you have control over the API that returns this result, you might consider changing that field to either be not present, or set to null if that field should be empty, instead of an empty array. Then you can simply change it back to [FlightItineraryDetails]?.