You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何利用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 14:32:06