skip to Main Content

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.

2

Answers


  1. The documentation of OrderedCollection describes which structure is expected for this type:

    OrderedDictionary expects its contents to be encoded as alternating key-value pairs in an unkeyed container.

    I.e. if you want to use OrderedCollection, you have to use a different JSON structure:

    [
        {
            "day": "Monday",
            "locationName": "Memorial Union Quad",
            "coordinate": [38.54141, -121.74845],
            "menu": [
                "Penne Pasta w/ Bolognese",
                ["🌾","🐷"],
                "Penne Pasta w/ Roasted Mushrooms",
                ["🌾","V"]
            ]
        }
    ]    
    

    In general, you should not use a JSON object but an array for menu if the order of the items is important. According to the JSON specification, the order of the properties of an object should not matter.

    If you can’t control how the JSON gets structured:

    Note that, as described above, there is the problem that the order of the JSON object properties should not matter.
    So the following solution is not recommended if the order matters!

    You can decode the JSON you have posted with the following object structure:

    struct Menu: Hashable {
        var id: UUID = { UUID() }
    
        let day: String
        let locationName: String
        let coordinate: [Double]
        let menu: [String: [String]]
    }
    

    If you have control over the JSON structure:

    There are several ways to structure or decode the data differently. Here is one alternative:

    struct Menu: Codable, Hashable {
        var id: UUID { UUID() }
    
        let day: String
        let locationName: String
        let coordinate: [Double]
        let menu: [MenuItem]
    }
    
    struct MenuItem: Codable, Hashable {
        let description: String
        let ingredients: [String]
    }
    

    With the JSON having the following structure:

    [
        {
            "day": "Monday",
            "locationName": "Memorial Union Quad",
            "coordinate": [38.54141, -121.74845],
            "menu": [
                {
                    "description": "Penne Pasta w/ Bolognese",
                    "ingredients": ["🌾","🐷"]
                },
                {
                    "description": "Penne Pasta w/ Roasted Mushrooms",
                    "ingredients": ["🌾","V"]
                }
            ]
        }
    ]
    

    Testing your code:

    I generally recommend writing a unit test for this type of problem / code, as this is the easiest way to test the functionality:

    import Testing
    
    struct MenuTests {
        @Test
        func testDecodingSucceedsAndReturnsProperData() async throws {
            let decoder = JSONDecoder()
            let menus = try decoder.decode([Menu].self, from: Self.jsonData)
    
            let menu = try #require(menus.first)
            #expect(menu.day == "Monday")
            #expect(menu.locationName == "Memorial Union Quad")
            #expect(menu.coordinate == [38.54141, -121.74845])
    
            #expect(menu.menu == [
                MenuItem(description: "Penne Pasta w/ Bolognese", ingredients: ["🌾","🐷"]),
                MenuItem(description: "Penne Pasta w/ Roasted Mushrooms", ingredients: ["🌾","V"]),
            ])
        }
    }
    
    private extension MenuTests {
        static let jsonData = """
        [
            {
                "day": "Monday",
                "locationName": "Memorial Union Quad",
                "coordinate": [38.54141, -121.74845],
                "menu": [
                    {
                        "description": "Penne Pasta w/ Bolognese",
                        "ingredients": ["🌾","🐷"]
                    },
                    {
                        "description": "Penne Pasta w/ Roasted Mushrooms",
                        "ingredients": ["🌾","V"]
                    }
                ]
            }
        ]    
        """.data(using: .utf8)!
    }
    
    Login or Signup to reply.
  2. 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"]
    ]
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search