代码之家  ›  专栏  ›  技术社区  ›  user28773992

在Swift中解析JSON错误:本应解码Array<Any>,但却找到了字典

  •  1
  • user28773992  · 技术社区  · 1 年前

    我正在尝试解码这个JSON 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"]
        }
      }
    ]
    

    这就是我在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 ""
        }
        
        
    }
    

    这就是我解码数据的方式:

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

    但我收到以下错误:“由于类型不匹配,无法从捆绑包中解码Menu.json。预期解码Array<Any>,但找到了一个字典。”

    我对数据建模正确吗?还是Bundle中的解码功能有问题?

    我尝试修改我建模数据的方式,但我是JSON新手,所以我不完全确定问题是否是我在Swift中建模JSON的方式。

    2 回复  |  直到 1 年前
        1
  •  1
  •   Sweeper    1 年前

    OrderedDictionary (从 swift-collections 大概)预计会从JSON中解码出来,如下所示:

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

    这就是为什么错误的说法是 Array<Any> 这是意料之中的。

    JSONDecoder 不保证in的密钥 KeyedDecodingContainer.allKeys 它们以与JSON字符串中显示的方式相同的方式排序 related issue 在快速收藏中,

    有序词典 需要保证其项的顺序在序列化/反序列化过程中不会改变。这不是由保证 Codable 的键控容器——因此它们不适合 有序词典 .

    请注意 可编码 是一种通用的归档解决方案,主要用于不关心序列化格式的精确细节的情况。这是Swift版本的 NSCoding ,或Python的 pickle . 可编码 它显然不是一个设计用于与任意JSON API互操作的灵活JSON编码器/解码器。

    因此,您要么需要使用自定义JSON解析库(我不知道有什么能保持顺序),而不是 可编码 ,或者将JSON的结构更改为 有序词典 预期:

    "menu": [
        "Penne Pasta w/ Bolognese",
        ["🌾","🐷"],
        "Penne Pasta w/ Roasted Mushrooms",
        ["🌾","V"]
    ]
    
        2
  •  1
  •   ITGuy    1 年前

    文件 OrderedCollection 描述了这种类型的预期结构:

    OrderedDictionary 期望其内容在未加密的容器中被编码为交替的键值对。

    即,如果你想使用 有序集合 ,您必须使用不同的JSON结构:

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

    一般来说,您不应该使用JSON对象,而应该使用数组 menu 如果物品的顺序很重要。根据JSON规范,对象属性的顺序应该无关紧要。

    如果你无法控制JSON的结构:

    请注意,如上所述,存在JSON对象属性的顺序不重要的问题。 因此,如果订单很重要,不建议使用以下解决方案!

    您可以使用以下对象结构对发布的JSON进行解码:

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

    如果您可以控制JSON结构:

    有几种方法可以不同地构造或解码数据。这里有一个替代方案:

    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]
    }
    

    JSON具有以下结构:

    [
        {
            "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"]
                }
            ]
        }
    ]
    

    测试你的代码:

    我通常建议为这类问题/代码编写一个单元测试,因为这是测试功能的最简单方法:

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