How to loop through json using CodingKeys and a custom decoder to build an array to assign to a struct property?

73 views Asked by At

I have a JSON object which I want to decode into a struct. The JSON has similar key:value pairs for ingredients and measurements. I'd like to condense the ingredient and measurement information into an array or pair of arrays in my struct.

I'm not sure how to loop through the data using CodingKeys and a custom decoder to build the array. I want to avoid creating multiple properties in my struct for each ingredient and measurement. Same for the CodingKeys; I want to avoid creating multiple cases for ingredient1, ..2, ..3, and so on.

I've tried using associated values in the CodingKeys enum and using a singleValueContainer. I get an assortment of errors that I'm not quite able to parse through.

Below is sample code and a sample of the JSON data.

JSON Sample

{ 
  "meal: [
   {
     "strMeal" : "name1",
     "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]
    // Could also be set up as var ingredients: [String] and var measurements: [String]
    
    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"
        // Not sure what case to include to address all variations of ingredient<#> and measurement<#>
    }
    
    enum RootCodingKeys: String, CodingKey {
        case meals
    }
    
    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 = ???
    }
}
1

There are 1 answers

2
workingdog support Ukraine On

try this approach using a while loop:

struct ApiResponse: Decodable {
    var meals: [Meal]?
}

struct Ingredient: Identifiable {
    let id = UUID()
    var name: String
    var measure: String
}

struct Meal: Decodable, Identifiable {
    var id: String
    
    var name: String?
    var drinkAlternate: String?
    var category: String?
    var area: String?
    var instructions: String?
    var mealThumb: String?
    var tags: String?
    var youtube: String?
    var source: String?
    var imageSource: String?
    var creativeCommonsConfirmed: String?
    var dateModified: String?
    
    var ingredients: [Ingredient]
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try values.decode(String.self, forKey: .id)
        self.name = try values.decodeIfPresent(String.self, forKey: .name)
        self.drinkAlternate = try values.decodeIfPresent(String.self, forKey: .drinkAlternate)
        self.category = try values.decodeIfPresent(String.self, forKey: .category)
        self.area = try values.decodeIfPresent(String.self, forKey: .area)
        self.instructions = try values.decodeIfPresent(String.self, forKey: .instructions)
        self.mealThumb = try values.decodeIfPresent(String.self, forKey: .mealThumb)
        self.tags = try values.decodeIfPresent(String.self, forKey: .tags)
        self.youtube = try values.decodeIfPresent(String.self, forKey: .youtube)
        self.source = try values.decodeIfPresent(String.self, forKey: .source)
        self.imageSource = try values.decodeIfPresent(String.self, forKey: .imageSource)
        self.creativeCommonsConfirmed = try values.decodeIfPresent(String.self, forKey: .creativeCommonsConfirmed)
        self.dateModified = try values.decodeIfPresent(String.self, forKey: .dateModified)
        
        self.ingredients = []
        let container = try decoder.singleValueContainer()
        let ingredDict = try container.decode([String: String?].self)
        var index = 1
        while
            let ingredient = ingredDict["strIngredient\(index)"] as? String,
            let measure = ingredDict["strMeasure\(index)"] as? String,
            !measure.isEmpty
        {
            self.ingredients.append(Ingredient(name: ingredient, measure: measure))
            index += 1
        }
    }
        
    enum CodingKeys: String, CodingKey {
        case id = "idMeal"
        case name = "strMeal"
        case drinkAlternate = "strDrinkAlternate"
        case category = "strCategory"
        case area = "strArea"
        case instructions = "strInstructions"
        case mealThumb = "strMealThumb"
        case tags = "strTags"
        case youtube = "strYoutube"
        case source = "strSource"
        case imageSource = "strImageSource"
        case creativeCommonsConfirmed = "strCreativeCommonsConfirmed"
        case dateModified
    }
  
}

Have a look at my test code on github using the themealdb: https://github.com/workingDog/FreeMeal/tree/main