如何利用CodingKeys和自定义Decoder遍历JSON构建数组赋值给结构体属性
问题描述
需要将包含多个ingredient<N>和measurement<N>键值对的JSON解码为Swift结构体,要求把这些食材和测量值整合为结构体中的数组,避免为每个序号的键单独定义CodingKeys case或结构体属性。
示例JSON:
{ "meal": [ { "idMeal": "1", "strMeal" : "name1", "strCategory": "category1", "strArea": "cuisine1", "strInstructions": "instructions1", "strMealThumb": "https://example.com/image.jpg", "ingredient1": "abc", "ingredient2": "efg", "measurement1": "m1", "measurement2": "m2" } ] }
现有结构体代码(存在疑问的部分已标注):
struct Recipe: Decodable { var id: String var name: String var category: String var cuisine: String var instructions: String var imageURL: URL var ingredients: [Ingredient] struct Ingredient { var ingredient: String var quantity: String } enum RecipeCodingKeys: String, CodingKey { case id = "idMeal" case name = "strMeal" case category = "strCategory" case cuisine = "strArea" case instructions = "strInstructions" case imageUrl = "strMealThumb" // 不知道如何处理所有ingredient<N>和measurement<N>的变体 } enum RootCodingKeys: String, CodingKey { case meals // 注意:JSON里是"meal",这里需要修正 } init(from decoder: Decoder) throws { let rootContainer = try decoder.container(keyedBy: RootCodingKeys.self) var arrayContainer = try rootContainer.nestedUnkeyedContainer(forKey: .meals) let propertiesContainer = try arrayContainer.nestedContainer(keyedBy: RecipeCodingKeys.self) self.id = try propertiesContainer.decode(String.self, forKey: .id) self.name = try propertiesContainer.decode(String.self, forKey: .name) self.category = try propertiesContainer.decode(String.self, forKey: .category) self.cuisine = try propertiesContainer.decode(String.self, forKey: .cuisine) self.instructions = try propertiesContainer.decode(String.self, forKey: .instructions) self.imageURL = try propertiesContainer.decode(URL.self, forKey: .imageUrl) self.ingredients = ??? // 这里不知道如何构建数组 } }
解决方案
核心思路是遍历JSON对象的所有键,筛选出食材和测量值的键对,再配对组装成数组,无需为每个序号单独定义CodingKeys。具体步骤如下:
1. 定义通用CodingKey处理未知键
创建一个可匹配任意字符串键的CodingKey枚举,用于访问JSON中未在RecipeCodingKeys中定义的ingredient<N>和measurement<N>:
private struct AnyCodingKey: CodingKey { var stringValue: String var intValue: Int? init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } init?(intValue: Int) { self.stringValue = String(intValue) self.intValue = intValue } }
2. 修正RootCodingKeys匹配JSON字段
JSON中根数组的键是meal,所以修正RootCodingKeys:
enum RootCodingKeys: String, CodingKey { case meal = "meal" }
3. 重写init(from:)方法完成解码
修改初始化方法,先获取整个meal对象的容器,解码已知字段后,遍历所有键筛选食材和测量值:
init(from decoder: Decoder) throws { // 解析根容器和meal数组 let rootContainer = try decoder.container(keyedBy: RootCodingKeys.self) var arrayContainer = try rootContainer.nestedUnkeyedContainer(forKey: .meal) let mealContainer = try arrayContainer.nestedContainer(keyedBy: AnyCodingKey.self) // 解码已知字段 self.id = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.id.rawValue)!) self.name = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.name.rawValue)!) self.category = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.category.rawValue)!) self.cuisine = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.cuisine.rawValue)!) self.instructions = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.instructions.rawValue)!) self.imageURL = try mealContainer.decode(URL.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.imageUrl.rawValue)!) // 初始化食材数组 var ingredients = [Ingredient]() // 遍历所有键,筛选出ingredient开头的键 for key in mealContainer.allKeys { guard key.stringValue.starts(with: "ingredient") else { continue } // 提取序号(比如从"ingredient1"中获取"1") let indexString = key.stringValue.replacingOccurrences(of: "ingredient", with: "") guard Int(indexString) != nil else { continue } // 获取对应的measurement键 let measurementKey = AnyCodingKey(stringValue: "measurement\(indexString)")! // 解码食材和测量值,过滤空值 guard let ingredientName = try mealContainer.decodeIfPresent(String.self, forKey: key), !ingredientName.isEmpty, let quantity = try mealContainer.decodeIfPresent(String.self, forKey: measurementKey), !quantity.isEmpty else { continue } ingredients.append(Ingredient(ingredient: ingredientName, quantity: quantity)) } self.ingredients = ingredients }
关键说明
- 使用
AnyCodingKey可以访问JSON中所有键,无需提前在RecipeCodingKeys中定义。 - 通过字符串匹配和序号提取,将
ingredientN和measurementN配对,避免重复代码。 - 加入空值过滤,防止API返回空字符串的食材/测量值混入数组。
完整代码整合:
struct Recipe: Decodable { var id: String var name: String var category: String var cuisine: String var instructions: String var imageURL: URL var ingredients: [Ingredient] struct Ingredient { var ingredient: String var quantity: String } enum RecipeCodingKeys: String, CodingKey { case id = "idMeal" case name = "strMeal" case category = "strCategory" case cuisine = "strArea" case instructions = "strInstructions" case imageUrl = "strMealThumb" } enum RootCodingKeys: String, CodingKey { case meal = "meal" } private struct AnyCodingKey: CodingKey { var stringValue: String var intValue: Int? init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } init?(intValue: Int) { self.stringValue = String(intValue) self.intValue = intValue } } init(from decoder: Decoder) throws { let rootContainer = try decoder.container(keyedBy: RootCodingKeys.self) var arrayContainer = try rootContainer.nestedUnkeyedContainer(forKey: .meal) let mealContainer = try arrayContainer.nestedContainer(keyedBy: AnyCodingKey.self) self.id = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.id.rawValue)!) self.name = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.name.rawValue)!) self.category = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.category.rawValue)!) self.cuisine = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.cuisine.rawValue)!) self.instructions = try mealContainer.decode(String.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.instructions.rawValue)!) self.imageURL = try mealContainer.decode(URL.self, forKey: AnyCodingKey(stringValue: RecipeCodingKeys.imageUrl.rawValue)!) var ingredients = [Ingredient]() for key in mealContainer.allKeys { guard key.stringValue.starts(with: "ingredient") else { continue } let indexString = key.stringValue.replacingOccurrences(of: "ingredient", with: "") guard Int(indexString) != nil else { continue } let measurementKey = AnyCodingKey(stringValue: "measurement\(indexString)")! guard let ingredientName = try mealContainer.decodeIfPresent(String.self, forKey: key), !ingredientName.isEmpty, let quantity = try mealContainer.decodeIfPresent(String.self, forKey: measurementKey), !quantity.isEmpty else { continue } ingredients.append(Ingredient(ingredient: ingredientName, quantity: quantity)) } self.ingredients = ingredients } }
内容的提问来源于stack exchange,提问作者Anonymous Nancy
相关产品推荐
相关产品推荐

