iosjsonswift

Parsing JSON in Swift Error: Expected to decode Array<Any> but found a dictionary instead


I'm trying to decode this JSON called Menu.json

[
  {
    "day": "Monday",
    "locationName": "Memorial Union Quad",
    "coordinate": [38.54141, -121.74845],
    "menu": {
        "Penne Pasta w/ Bolognese" : ["🌾","🐷"],
        "Penne Pasta w/ Roasted Mushrooms" : ["🌾","V"]
    }
  },
  {
    "day": "Tuesday",
    "locationName": "International Center",
    "coordinate": [38.54540, -121.75494],
    "menu": {
        "Beef & Lamb Gyro" : ["🌾","🫛", "🥛"],
        "Edamame Hummus w/ Veggies" : ["🫛","🥛", "SE", "VE"]
    }
  },
  {
    "day": "Wednesday",
    "locationName": "Storer Hall",
    "coordinate": [38.54114, -121.75461],
    "menu": {
        "Seafood Salad Tostada" : ["🥚","🦐", "🫛", "🐟"],
        "Southwest Black Bean Tostada" : ["V"]
    }
  },
  {
    "day": "Thursday",
    "locationName": "Orchard Park",
    "coordinate": [38.544828, -121.765170],
    "menu": {
        "Teriyaki Beef w/ Stir Fry Noodles" : ["🌾","🫛", "SE"],
        "Teriyaki Tofu w/ Veggie Stir Fry" : ["🌾","🫛", "SE","V"]
    }
  },
   {
    "day": "Friday",
    "locationName": "Memorial Union Quad",
    "coordinate": [38.54141, -121.74845],
    "menu": {
        "Soy Ciltrano Lime Chicken" : ["🌾","🫛"],
        "Southwest Tofu" : ["🫛","V"]
    }
  }
]

This is how I'm modeling the data in Swift:

import Foundation
import OrderedCollections

struct Menu : Codable, Hashable {
    var id: UUID { UUID() }
    let day: String
    let locationName: String
    let coordinate: [Double]
    let menu: OrderedDictionary<String, [String]>
    
    func getTodaysLocation(_ today: String)-> String{
        if today == day{
            return locationName
        }
        return ""
    }
    
    
}

And this is how I'm decoding the data:

import Foundation

extension Bundle {
    func decode(_ file: String) -> [Menu] {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()

        do {
            return try decoder.decode([Menu].self, from: data)
        } catch DecodingError.keyNotFound(let key, let context) {
            fatalError("Failed to decode \(file) from bundle due to missing key '\(key.stringValue)' – \(context.debugDescription)")
        } catch DecodingError.typeMismatch(_, let context) {
            fatalError("Failed to decode \(file) from bundle due to type mismatch – \(context.debugDescription)")
        } catch DecodingError.valueNotFound(let type, let context) {
            fatalError("Failed to decode \(file) from bundle due to missing \(type) value – \(context.debugDescription)")
        } catch DecodingError.dataCorrupted(_) {
            fatalError("Failed to decode \(file) from bundle because it appears to be invalid JSON.")
        } catch {
            fatalError("Failed to decode \(file) from bundle: \(error.localizedDescription)")
        }
    }
}

But I get the following error: "Failed to decode Menu.json from bundle due to type mismatch – Expected to decode Array<Any> but found a dictionary instead."

Am I modeling the data correctly? Or is it an issue with the decode function in the Bundle?

I tried modifying the way I'm modeling the data, but I new to JSON, so I'm not totally sure if the issue is the way I modeled the JSON in Swift.


Solution

  • OrderedDictionary (from swift-collections presumably) is expected to be decoded from a JSON like this:

    [
        "key1",
        "value1",
        "key2",
        "value2"
    ]
    

    This is why the error is saying that an Array<Any> is expected.

    JSONDecoder does not guarantee that the keys of in KeyedDecodingContainer.allKeys are ordered in the same way they appear in the JSON string. From a related issue in swift-collections,

    OrderedDictionary needs to guarantee that the order of its items will not change during serialization/deserialization. This is not guaranteed by Codable's keyed containers -- therefore they aren't suitable for use by OrderedDictionary.

    Note that Codable is a common denominator archival solution, primarily intended for use where you don't care about the precise details of the serialized format. It is the Swift version of NSCoding, or Python's pickle. Codable is emphatically not a flexible JSON encoder/decoder that's designed to interoperate with arbitrary JSON APIs.

    So you either need to use a custom JSON parsing library (I don't know of any that preserves the order) instead of Codable, or change the structure of your JSON to what the OrderedDictionary expects:

    "menu": [
        "Penne Pasta w/ Bolognese",
        ["🌾","🐷"],
        "Penne Pasta w/ Roasted Mushrooms",
        ["🌾","V"]
    ]