Swift解析Menu.json报错:期望解码数组却发现字典的问题排查
问题背景
尝试解析名为Menu.json的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 "" } }
使用Bundle扩展的decode函数解码:
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)") } } }
运行后出现错误:
"Failed to decode Menu.json from bundle due to type mismatch – Expected to decode Array
but found a dictionary instead."
错误原因
问题出在OrderedDictionary<String, [String]>的默认Codable实现上。OrderedDictionary默认会将自身编码/解码为键值对数组(格式类似[{"key": "...", "value": "..."}]),但你的JSON中menu字段是标准的JSON对象(键值对直接嵌套),两者格式不匹配,导致解码器期望数组却收到字典,触发类型不匹配错误。
解决方案
方案1:改用普通Dictionary(无需保持顺序)
如果不需要保留菜单条目的顺序,直接将menu字段类型改为Dictionary<String, [String]>即可,Swift标准Dictionary默认支持JSON对象的解码:
import Foundation struct Menu : Codable, Hashable { var id: UUID { UUID() } let day: String let locationName: String let coordinate: [Double] // 替换OrderedDictionary为普通Dictionary let menu: [String: [String]] func getTodaysLocation(_ today: String)-> String{ if today == day{ return locationName } return "" } }
方案2:自定义OrderedDictionary的Codable实现(需保持顺序)
如果必须保留菜单条目的顺序,需要为OrderedDictionary扩展自定义的Codable逻辑,使其能从JSON对象解码:
import Foundation import OrderedCollections // 扩展OrderedDictionary,支持从JSON对象解码/编码 extension OrderedDictionary: Codable where Key: CodingKey & Hashable, Value: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: Key.self) self.init() for key in container.allKeys { let value = try container.decode(Value.self, forKey: key) self.updateValue(value, forKey: key) } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: Key.self) for (key, value) in self { try container.encode(value, forKey: key) } } } // 原模型保持不变 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 "" } }
注意:此扩展要求Key类型遵循CodingKey,String默认符合该协议,所以OrderedDictionary<String, [String]>可以正常使用。
验证修改
修改完成后,重新调用Bundle.decode("Menu.json")即可正常解码JSON数据。
内容的提问来源于stack exchange,提问作者user28773992

