Perfect Aduh
Perfect Aduh

Reputation: 43

Codable Error - Expected to decode Dictionary <String, Any> but found array

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
    )
)

Upvotes: 0

Views: 645

Answers (1)

Evan Deaubl
Evan Deaubl

Reputation: 719

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]?.

Upvotes: 1

Related Questions