Search code examples
swiftalamofirecodabledecodable

Alamofire & Codable Issue Parsing Responses


I have tried a handful of ways to try and get my models to populate from this Alamofire GET call. Not sure what I am missing. I will include the JSON that is coming over as "data" as well.

func fetchMeals(){
    
    let headers:HTTPHeaders = [
        "x-rapidapi-key": "XXXXXXXXX",
        "x-rapidapi-host": "spoonacular-recipe-food-nutrition-v1.p.rapidapi.com"
    ]
    
    let parameters = ["timeframe":"day", "targetCalories":"2000", "diet":"vegetarian"]
    
    let request = AF.request("https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com/recipes/mealplans/generate", parameters: parameters, headers: headers).responseData { response in switch response.result {
    case .success(let data):
        
        print(JSON(data))
        
        let recipes = try? JSONDecoder().decode(Recipes.self, from: data)
        if let recipes = recipes {
            print(recipes.items[0].day)
        }
        //debugPrint(json
        //let recipes = try! JSONDecoder().decode([Recipes].self, from: data as? Any)
            //print(recipes)
        
    case .failure(let error):
        print(error)
        
    }

Here is what is returned as data:

{
  "publishAsPublic" : true,
  "items" : [
    {
      "slot" : 1,
      "type" : "RECIPE",
      "day" : 1,
      "mealPlanId" : 0,
      "value" : "{\"id\":1509199,\"imageType\":\"jpg\",\"title\":\"Our Favorite Zucchini Bread\"}",
      "position" : 0
    },
    {
      "slot" : 2,
      "type" : "RECIPE",
      "day" : 1,
      "mealPlanId" : 0,
      "value" : "{\"id\":426849,\"imageType\":\"jpg\",\"title\":\"Quick Eggnog French Toast\"}",
      "position" : 0
    },
    {
      "slot" : 3,
      "type" : "RECIPE",
      "day" : 1,
      "mealPlanId" : 0,
      "value" : "{\"id\":844348,\"imageType\":\"jpg\",\"title\":\"Tiramisu Cake\"}",
      "position" : 0
    },
    {
      "slot" : 1,
      "type" : "RECIPE",
      "day" : 2,
      "mealPlanId" : 0,
      "value" : "{\"id\":715569,\"imageType\":\"jpg\",\"title\":\"Strawberry Cheesecake Chocolate Crepes\"}",
      "position" : 0
    },
    {
      "slot" : 2,
      "type" : "RECIPE",
      "day" : 2,
      "mealPlanId" : 0,
      "value" : "{\"id\":484174,\"imageType\":\"jpg\",\"title\":\"French Toast Waffles\"}",
      "position" : 0
    },
    {
      "slot" : 3,
      "type" : "RECIPE",
      "day" : 2,
      "mealPlanId" : 0,
      "value" : "{\"id\":894915,\"imageType\":\"jpg\",\"title\":\"Paleo Lemon Bars [VIDEO]\"}",
      "position" : 0
    },
    {
      "slot" : 1,
      "type" : "RECIPE",
      "day" : 3,
      "mealPlanId" : 0,
      "value" : "{\"id\":514551,\"imageType\":\"jpg\",\"title\":\"Overnight French Toast Casserole: A perfect make-ahead breakfast\"}",
      "position" : 0
    },
    {
      "slot" : 2,
      "type" : "RECIPE",
      "day" : 3,
      "mealPlanId" : 0,
      "value" : "{\"id\":88612,\"imageType\":\"png\",\"title\":\"Marc Vetri's Rigatoni with Swordfish, Tomato, and Eggplant Fries\"}",
      "position" : 0
    },
    {
      "slot" : 3,
      "type" : "RECIPE",
      "day" : 3,
      "mealPlanId" : 0,
      "value" : "{\"id\":345768,\"imageType\":\"jpeg\",\"title\":\"The Neely's Caprese Tart\"}",
      "position" : 0
    },
    {
      "slot" : 1,
      "type" : "RECIPE",
      "day" : 4,
      "mealPlanId" : 0,
      "value" : "{\"id\":377285,\"imageType\":\"jpg\",\"title\":\"Blueberry Brunch Bake\"}",
      "position" : 0
    },
    {
      "slot" : 2,
      "type" : "RECIPE",
      "day" : 4,
      "mealPlanId" : 0,
      "value" : "{\"id\":159846,\"imageType\":\"jpg\",\"title\":\"Best Eggnog\"}",
      "position" : 0
    },
    {
      "slot" : 3,
      "type" : "RECIPE",
      "day" : 4,
      "mealPlanId" : 0,
      "value" : "{\"id\":633165,\"imageType\":\"jpg\",\"title\":\"Avocado Tomato & Mozzarella Panini\"}",
      "position" : 0
    },
    {
      "slot" : 1,
      "type" : "RECIPE",
      "day" : 5,
      "mealPlanId" : 0,
      "value" : "{\"id\":1096246,\"imageType\":\"jpg\",\"title\":\"Apple Cinnamon Quiche\"}",
      "position" : 0
    },
    {
      "slot" : 2,
      "type" : "RECIPE",
      "day" : 5,
      "mealPlanId" : 0,
      "value" : "{\"id\":484238,\"imageType\":\"jpg\",\"title\":\"Spinach Scramble\"}",
      "position" : 0
    },
    {
      "slot" : 3,
      "type" : "RECIPE",
      "day" : 5,
      "mealPlanId" : 0,
      "value" : "{\"id\":471864,\"imageType\":\"jpg\",\"title\":\"Teeny Lamothe's Pear & Goat Cheese Tart\"}",
      "position" : 0
    },
    {
      "slot" : 1,
      "type" : "RECIPE",
      "day" : 6,
      "mealPlanId" : 0,
      "value" : "{\"id\":1118497,\"imageType\":\"jpg\",\"title\":\"Strawberry Oatmeal\"}",
      "position" : 0
    },
    {
      "slot" : 2,
      "type" : "RECIPE",
      "day" : 6,
      "mealPlanId" : 0,
      "value" : "{\"id\":1005954,\"imageType\":\"jpg\",\"title\":\"Simply Delicious\"}",
      "position" : 0
    },
    {
      "slot" : 3,
      "type" : "RECIPE",
      "day" : 6,
      "mealPlanId" : 0,
      "value" : "{\"id\":555798,\"imageType\":\"jpg\",\"title\":\"Black Forest Cheesecake Trifles\"}",
      "position" : 0
    },
    {
      "slot" : 1,
      "type" : "RECIPE",
      "day" : 7,
      "mealPlanId" : 0,
      "value" : "{\"id\":525517,\"imageType\":\"jpeg\",\"title\":\"Slim & Healthy Ways to Cook Oatmeal for Breakfast\"}",
      "position" : 0
    },
    {
      "slot" : 2,
      "type" : "RECIPE",
      "day" : 7,
      "mealPlanId" : 0,
      "value" : "{\"id\":491182,\"imageType\":\"jpg\",\"title\":\"Cheesy Salsa Omelet\"}",
      "position" : 0
    },
    {
      "slot" : 3,
      "type" : "RECIPE",
      "day" : 7,
      "mealPlanId" : 0,
      "value" : "{\"id\":1130503,\"imageType\":\"jpg\",\"title\":\"Easy English Muffin Pizza\"}",
      "position" : 0
    }
  ],
  "name" : "MealPlan 1607269962128"
}
}

Here are my models:

struct Value : Decodable {
    
    let id : Int
    let imageType :String
    let title :String
    
}

struct Recipe : Decodable {
    
    let type :String
    let position :Int
    let value :Value
    let slot :Int
    let mealPlanId :Int
    let day :Int
    
}

struct Recipes : Decodable {
    
    let publishAsPublic :Bool
    let items: [Recipe]
    let name :String
    
}

When I print the variable as above, it returns nil. Any help is appreciated.


Solution

  • First of all, never try? the decoding process. This way you are missing critical information about the failure of the process.

    The only issue with your model is that you are trying to map value property as Value type but it clearly is of String type.

    You can workaround this be passing in your JSONDecoder, the nested decoder that you will use to decode value string, using userInfo:

    extension CodingUserInfoKey {
        static let nestedDecoder = CodingUserInfoKey(rawValue: "com.app.NestedDecoder")!
    }
    
    let decoder = JSONDecoder()
    decoder.userInfo = [
        .nestedDecoder: JSONDecoder()
    ]
    do {
        let decoded = try decoder.decode(Recipes.self, from: data)
        print(decoded)
    } catch {
        print(error)
    }
    

    And then use that decoder in a custom implementation of init(from:) initializer, like this:

    enum RecipeDecodingError: Error {
        case noNestedDecoder
    }
    
    struct Recipe: Decodable {
        let type: String
        let position: Int
        let value: Value
        let slot: Int
        let mealPlanId: Int
        let day: Int
        
        enum CodingKeys: String, CodingKey {
            case type
            case position
            case value
            case slot
            case mealPlanId
            case day
        }
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            type = try container.decode(String.self, forKey: .type)
            position = try container.decode(Int.self, forKey: .position)
            
            let stringValue = try container.decode(String.self, forKey: .value)
            guard let nestedDecoder = decoder.userInfo[.nestedDecoder] as? JSONDecoder else {
                throw RecipeDecodingError.noNestedDecoder
            }
            value = try nestedDecoder.decode(Value.self, from: Data(stringValue.utf8))
            
            slot = try container.decode(Int.self, forKey: .slot)
            mealPlanId = try container.decode(Int.self, forKey: .mealPlanId)
            day = try container.decode(Int.self, forKey: .day)
        }
    }