0

I am new to Swift and I am having an issue with an application that I am developing. I am trying to parse some JSON provided by an API and I am encountering an issue where the program fails to decode the JSON into Swift Types provided by a struct.

Recipe_API_Caller.swift

 import Foundation
 import UIKit

class call_Edamam_API{

func fetch(matching query: [String: String], completion: @escaping ([Recipe]?)-> Void){
        let baseURL = URL(string: "https://api.edamam.com/search")!

        guard let url = baseURL.withQueries(query)
            else{
                completion(nil)
                print("Cannot build URL")
                return
    }
    print(url)


        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in

            let Recipe_Decoder = JSONDecoder()

            if let data = data,

                let Recipes = try?
                    Recipe_Decoder.decode(Hit.self, from: data){

                completion(Recipes.hits)
            }
            else{
                print("JSON Decoding Error")
                completion(nil)
                return
            }
        }

        task.resume()


}
}

Recipe.swift

import Foundation
struct Hit: Codable{
    let hits: [Recipe]
}

struct Recipe: Codable{
    var name: String
    var image: URL
    var Ingredient_List: Array<String>
    var See_More_URL: URL

    enum Recipe_Coding_Keys: String, CodingKey{
        case name = "label"
        case image = "image"
        case Ingredient_List = "ingredientLines"
        case See_More_URL = "url"
    }

    init(from decoder: Decoder) throws {
        let recipe_Info = try decoder.container(keyedBy: Recipe_Coding_Keys.self)

        name = try recipe_Info.decode(String.self, forKey: Recipe_Coding_Keys.name)
        image = try recipe_Info.decode(URL.self, forKey: Recipe_Coding_Keys.image)
        Ingredient_List = try recipe_Info.decode(Array.self, forKey: Recipe_Coding_Keys.Ingredient_List)
        See_More_URL = try recipe_Info.decode(URL.self, forKey: Recipe_Coding_Keys.See_More_URL)

        print(name)
        print(See_More_URL)
    }
}

RecipeTableViewController.swift

import UIKit

class RecipeTableViewController: UITableViewController {

    @IBAction func downloadButtonTouched(_ sender: Any) {
    }




    let recipe_API_Call = call_Edamam_API()

    var returned_Recipes = [Recipe]()



    override func viewDidLoad() {
        super.viewDidLoad()
        print(searchTermImport)
        fetchRecipes()
    }

    func fetchRecipes(){
        self.returned_Recipes = []
        self.tableView.reloadData()

        let query: [String: String] = [
            "q": "Chicken",
            "app_id": "xxx",
            "app_key": "xxx"
        ]

        recipe_API_Call.fetch(matching: query, completion: {(returned_Recipes) in
            DispatchQueue.main.async {
                if let returned_Recipes = returned_Recipes{
                    self.returned_Recipes = returned_Recipes
                    self.tableView.reloadData()
                }
                else{
                    print("Fetch Error")
                }
            }
        })
    }

    func configureTable(cell: UITableViewCell, forItemAt indexPath: IndexPath){
        let returned_Recipe = returned_Recipes[indexPath.row]

        cell.textLabel?.text = returned_Recipe.name

        let network_task = URLSession.shared.dataTask(with: returned_Recipe.image){ (data, response, error)
            in
            guard let Image_dest = data else{
                return
            }
            DispatchQueue.main.async {
                let image = UIImage(data: Image_dest)
                cell.imageView?.image = image
            }
        }
        network_task.resume()
    }

    // MARK: - Table view data source


    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return returned_Recipes.count
    }


    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "recipeCell", for: indexPath)

        // Configure the cell...
        configureTable(cell: cell, forItemAt: indexPath)
        return cell
    }

Part of JSON retrieved from URL

{
    "q" : "Chicken",
    "from" : 0,
    "to" : 10,
    "more" : true,
    "count" : 168106,
    "hits" : [ {
        "recipe" : {
            "uri" : "http://www.edamam.com/ontologies/edamam.owl#recipe_b79327d05b8e5b838ad6cfd9576b30b6",
            "label" : "Chicken Vesuvio",
            "image" : "https://www.edamam.com/web-img/e42/xxx.jpg",
            "source" : "Serious Eats",
            "url" : "http://www.seriouseats.com/recipes/2011/12/chicken-vesuvio-recipe.html",
            "shareAs" : "http://www.edamam.com/recipe/chicken-vesuvio-b79327d05b8e5b838ad6cfd9576b30b6/chicken",
            "yield" : 4.0,
            "dietLabels" : [ "Low-Carb" ],
            "healthLabels" : [ "Sugar-Conscious", "Peanut-Free", "Tree-Nut-Free" ],
            "cautions" : [ "Sulfites" ],
            "ingredientLines" : [ "1/2 cup olive oil", "5 cloves garlic, peeled", "2 large russet potatoes, peeled and cut into chunks", "1 3-4 pound chicken, cut into 8 pieces (or 3 pound chicken legs)", "3/4 cup white wine", "3/4 cup chicken stock", "3 tablespoons chopped parsley", "1 tablespoon dried oregano", "Salt and pepper", "1 cup frozen peas, thawed" ],

Upon running the app, the program receives data from the API call, but it is unable to decode it into Swift Types that I created in Recipe.swift. I receive the "JSON Decoding Error" that I created in Recipe_API_Caller.swift. I thought my issue may have been that I didn't have another struct for the "hits" part of the JSON file, but I added that and it is still giving me the error.

I can print the created URL and input it into a web browser and browse the JSON file manually so I know my URL and API key are working properly. Xcode does not provide me with any official errors as to what is happening, so I am unsure of how to proceed from here. I would appreciate any advice that you all could provide!

1
  • The JSON Decoding Error usually comes with an explanation of the specific key that caused the issue. You can look at it to get more info on the error and maybe add it to the question to allow others to better understand your situation. Also, ViewController and fetch implementation are probably not relevant to the problem. Consider, instead, to use your space with more of the JSON you are returning. Commented May 4, 2020 at 8:48

3 Answers 3

1

The real problem with your code is the non-existent error handling when decoding, you need to rewrite that part so that it catches and prints any errors

if let data = data {
    do {
        let Recipes = try Recipe_Decoder.decode(Hit.self, from: data)
        completion(Recipes.hits)
    }
    else {
         print(error)
         completion(nil)
    }
}

If you change to this type of error handling you will get a lot of information from the error which in this case was

keyNotFound(Recipe_Coding_Keys(stringValue: "label", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "hits", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key Recipe_Coding_Keys(stringValue: \"label\", intValue: nil) (\"label\").", underlyingError: nil))

Which tells us that the decoder can't find the key "label" inside "hits" and this is because you are missing one level in your data structure for the key "recipe"

Sign up to request clarification or add additional context in comments.

4 Comments

Ok. I have implemented your solution and I am now receiving errors properly. Thank you for that! I have another question, particularly about decode and completion. I have also implemented PGDev's Codable Structs, so how does decode and completion need to change? Does it need to start decoding at the "top" of the structs (Root Struct) or something else entirely?
@Gamerprime Decoding must start at the top level so Root.self should be used as argument to the decoder but the call to the completion handler is the same.
Ok, I made the changes to the decoder and changed the argument to Root.self and it is giving me another error. "Cannot convert value of type '[Hit]' to expected argument type '[Recipe]?'" Does the @escaping argument of the fetch function need to change as well?
@Gamerprime Yes you need to change the type to [Hit] and then you need to handle the new type inside the completion handler.
1

The problem lies in structural differences between the JSON and your structs. The hits key in the JSON contains an array of objects that (in the given snippet) only have a recipe key which then contains your Recipe object. You try to decode the Recipe directly inside the hits array, though.

The following models file should work:

struct Hit: Codable{
    let hits: [RecipeHit]
}

struct RecipeHit: Codable {
    let recipe: Recipe
}

struct Recipe: Codable{
    var name: String
    var image: URL
    var Ingredient_List: Array<String>
    var See_More_URL: URL

    enum Recipe_Coding_Keys: String, CodingKey{
        case name = "label"
        case image = "image"
        case Ingredient_List = "ingredientLines"
        case See_More_URL = "url"
    }

    init(from decoder: Decoder) throws {
        let recipe_Info = try decoder.container(keyedBy: Recipe_Coding_Keys.self)

        name = try recipe_Info.decode(String.self, forKey: Recipe_Coding_Keys.name)
        image = try recipe_Info.decode(URL.self, forKey: Recipe_Coding_Keys.image)
        Ingredient_List = try recipe_Info.decode(Array.self, forKey: Recipe_Coding_Keys.Ingredient_List)
        See_More_URL = try recipe_Info.decode(URL.self, forKey: Recipe_Coding_Keys.See_More_URL)

        print(name)
        print(See_More_URL)
    }
}



Note: The following part is just a little heads-up and not necessary for the code above to work!
You can save some lines of code by using Swift's coding/naming conventions and letting the compiler generate the initializer for you... :-)

struct Recipe: Codable {
    private enum CodingKeys: String, CodingKey {
        case name = "label"
        case image
        case ingredientList = "ingredientLines"
        case seeMoreURL = "url"
    }

    var name: String
    var image: URL
    var ingredientList: Array<String>
    var seeMoreURL: URL
}

Comments

0

Your Codable types to parse the above JSON format should be,

struct Root: Decodable {
    let hits: [Hit]
}

struct Hit: Decodable {
    let recipe: Recipe
}

struct Recipe: Decodable {
    let name: String
    let image: URL
    let ingredientList: [String]
    let seeMoreUrl: URL

    enum CodingKeys: String, CodingKey {
        case name = "label"
        case image
        case ingredientList = "ingredientLines"
        case seeMoreUrl = "url"
    }
}

Some key points to remember while using Codable for parsing,

  1. Use enum CodingKeys instead of any other custom name. init(from:) will automatically pick the keys from enum CodingKeys and use while parsing.
  2. No need to explicitly implement init(from:) if you don't have any specific parsing implementation.
  3. Use camel-case while defining the variables in Swift. Example: Use ingredientList instead of Ingredient_List.
  4. Use optional types wherever you expect the API to return null.
  5. Use Decodable/Encodable if you have one of the decoding/encoding requirements (not sure about yours).

Note: Make the code as short as possible.

1 Comment

Thank you! I didn't realize that the init(from:) was not needed in this case. Thanks again!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.