Search code examples
iosswiftasync-awaitjsondecoderyelp-fusion-api

Why is async method throwing an error when decoding JSON data from an API request? Swift


Tldr:

*Update: Updated code and post body.

I’m trying to show a table view in a new view controller, but I keep getting an error thrown from the async method searchBusinesses in the YelpApi class in FetchData.swift.

I’ve printed the JSON response after let (data, _) = try await URLSession.shared.data(for: request) in FetchData.swift, however, the response looks fine/there doesn’t seem to be any errors or indications of a problem in it (a snippet of this response is at the bottom of the Returned Print Statement "file" at the bottom of this post).

I've also printed the error description message at the bottom of this post in the Returned Print Statement "file".

Main thing I think the problem is related to:

-Code wrong somehow in code for decoding data from API request in line of code: let businessResults = try JSONDecoder().decode(BusinessSearchResult.self, from: data) in searchBusinesses method in FetchData.swift.


Rest of post, including all code “files”:

This is a follow-up to the following question: Why isn’t the table view from a new View Controller not showing? Swift.

I’m trying to show a table view in a new view controller, but I keep getting an error thrown from the async method searchBusinesses in the YelpApi class in FetchData.swift.

I also looked this problem up online, and re-read the documentation for the following topics (with the exception of JSONDecoder(); first time reading it), and still could not identify the problem:

-Error Handling https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html

-URLSession https://developer.apple.com/documentation/foundation/urlsession

-JSONDecoder() https://developer.apple.com/documentation/foundation/jsondecoder

I think the problem could be:

-The function for generating the openAt time parameter returns a value that somehow causes an error during the API request, and/or when decoding the data. However the print statements within this function for generating the openAt time indicates that the code within this function is working as it should.

-Code wrong somehow when making the API request in line of code: let (data, _) = try await URLSession.shared.data(for: request) in searchBusinesses method (in the YelpApi class in FetchData.swift).

-Code wrong somehow in code for decoding data from API request in line of code: let businessResults = try JSONDecoder().decode(BusinessSearchResult.self, from: data) in searchBusinesses method.

*Note about code changes: I had recently changed the location property in my Venues struct which conforms to Codable to locationOfRestaurant and had forgot to update the code "file" here to reflect that. After receiving errors after this change, I've since changed it back to location. *The error description message at the bottom of this post referred to this locationOfRestaurant property.

Code:

InitialViewController.swift:

//*Code for creating a table view that shows options to the user, for the user to select.*

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        //*Code for assigning values to variables related to what row in the table view the user selected.*
        
        
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let newVC = storyboard.instantiateViewController(identifier: "NewViewController") as! NewViewController
        newVC.modalPresentationStyle = .fullScreen
        newVC.modalTransitionStyle = .crossDissolve
        
        //Print Check.
        //Prints.
        print("Print Check: Right before code for presenting the new view controller.")

        navigationController?.pushViewController(newVC, animated: true)
        
    }

NewViewController.swift:

import UIKit
import CoreLocation

class NewViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    //Print Check.
    //Prints.
    func printCheckBeforeIBOutletTableViewCode() {
        print("Print Check: Right before tableView IBOutlet code at top of NewViewController.swift file.")
    }
    
    @IBOutlet var tableView: UITableView!
    
    var venues: [Venue] = []
    
    //Print Check.
    //Prints.
    func printCheckAfterIBOutletTableViewCode() {
        print("Print Check: Right after tableView IBOutlet code at top of NewViewController.swift file.")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //Function calls for print checks.
        //Prints.
        self.printCheckBeforeIBOutletTableViewCode()
        self.printCheckAfterIBOutletTableViewCode()
        
        tableView.register(UINib(nibName: "CustomTableViewCell", bundle: nil), forCellReuseIdentifier: "CustomTableViewCell")
        tableView.delegate = self
        tableView.dataSource = self
        
        //Print Check.
        //Prints.
        print("Print Check: Right before creating an instance of YelpApi class, then creating a task to make the API request.")
        
        let yelpApi = YelpApi(apiKey: "Api key")
        
        Task {
            do {
                self.venues = try await yelpApi.searchBusiness(latitude: selectedLatitude, longitude: selectedLongitude, category: "category query goes here", sortBy: "sort by query goes here", openAt: functionForGeneratingOpenAtParameter())
                self.tableView.reloadData()
            } catch {
                    //Handle error here.
                    //Prints.
                    print("Error info for catch block in Task block in NewViewController.swift for making API request: \(error)")
            }
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
           return venues.count
       }
       
       func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
           let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath) as! CustomTableViewCell
           
           //Details for custom table view cell go here.
       }
           
       //Rest of table view protocol functions.
    
}

Venue.swift:

import Foundation

// MARK: - BusinessSearchResult
struct BusinessSearchResult: Codable {
    let total: Int
    let businesses: [Venue]
    let region: Region
}

// MARK: - Business
struct Venue: Codable {
    let rating: Double
    let price, phone, alias: String?
    let id: String
    let isClosed: Bool?
    let categories: [Category]
    let reviewCount: Int?
    let name: String
    let url: String?
    let coordinates: Center
    let imageURL: String?
    let location: Location
    let distance: Double
    let transactions: [String]

    enum CodingKeys: String, CodingKey {
        case rating, price, phone, id, alias
        case isClosed
        case categories
        case reviewCount
        case name, url, coordinates
        case imageURL
        case location, distance, transactions
    }
}

// MARK: - Category
struct Category: Codable {
    let alias, title: String
}

// MARK: - Center
struct Center: Codable {
    let latitude, longitude: Double
}

// MARK: - Location
struct Location: Codable {
    let city, country, address2, address3: String?
    let state, address1, zipCode: String?

    enum CodingKeys: String, CodingKey {
        case city, country, address2, address3, state, address1
        case zipCode
    }
}

// MARK: - Region
struct Region: Codable {
    let center: Center
}

FetchData.swift:

import Foundation
import CoreLocation

class YelpApi {
    
    private var apiKey: String
    
    init(apiKey: String) {
        self.apiKey = apiKey
    }
    
    func searchBusiness(latitude: Double,
                        longitude: Double,
                        category: String,
                        sortBy: String,
                        openAt: Int?) async throws -> [Venue] {
        
        var queryItems = [URLQueryItem]()
        queryItems.append(URLQueryItem(name:"latitude",value:"\(latitude)"))
        queryItems.append(URLQueryItem(name:"longitude",value:"\(longitude)"))
        queryItems.append(URLQueryItem(name:"categories", value:category))
        queryItems.append(URLQueryItem(name:"sort_by",value:sortBy))
        
        if let openAt = openAt {
            queryItems.append(URLQueryItem(name:"open_at", value:"\(openAt)"))
        }
       
        var results = [Venue]()
        
        var expectedCount = 0
        let countLimit = 50
        var offset = 0
        
        queryItems.append(URLQueryItem(name:"limit", value:"\(countLimit)"))
        
        //Print Check.
        //Prints.
        print("Print Check: Line before repeat-while loop.")
        
        repeat {
            
            //Print Check.
            //Prints.
            print("Print Check: Within repeat-while loop and before first line of code within it.")
            
            var offsetQueryItems = queryItems
            
            offsetQueryItems.append(URLQueryItem(name:"offset",value: "\(offset)"))
            
            var urlComponents = URLComponents(string: "https://api.yelp.com/v3/businesses/search")
            urlComponents?.queryItems = offsetQueryItems
            
            guard let url = urlComponents?.url else {
                throw URLError(.badURL)
            }
            
            var request = URLRequest(url: url)
            request.setValue("Bearer \(self.apiKey)", forHTTPHeaderField: "Authorization")
            
            //Print Check.
            //Prints.
            print("Print Check: Within repeat-while loop and before 'let (data, _) = try await' line of code.")
            let (data, _) = try await URLSession.shared.data(for: request)
            
            //Print Check for printing the JSON response.
            //Prints.
            print(String(decoding: data, as: UTF8.self))
            
            //Print Check.
            //Prints.
            print("Print Check: Within repeat-while loop and before 'let businessResults = try JSONDecoder()' line of code.")
            let businessResults = try JSONDecoder().decode(BusinessSearchResult.self, from:data)
            
            //Print Check.
            //Doesn't print.
            print("Print Check: Within repeat-while loop and right after 'let businessResults = try JSONDecoder()' line of code.")

            expectedCount = min(businessResults.total,1000)
            
            results.append(contentsOf: businessResults.businesses)
            offset += businessResults.businesses.count
        } while (results.count < expectedCount)
        
        //Print Check.
        //Doesn't print.
        print("Print Check: After repeat-while loop and before 'return results' code.")
        
        return results
    }
}

Returned Print Statements From Terminal, including the JSON response and catch block error description at the end:

Print Check: Right before code for presenting the new view controller.
Print Check: Right before tableView IBOutlet code at top of NewViewController.swift file.
Print Check: Right after tableView IBOutlet code at top of NewViewController.swift file.
Print Check: Right before creating an instance of YelpApi class, then creating a task to make the API request.
Print statements from functionForGeneratingOpenAtParameter(). These print statements indicate the function is working as it should, and don't seem to indicate a problem.
Print Check: Line before repeat-while loop.
Print Check: Within repeat-while loop and before first line of code within it.
Print Check: Within repeat-while loop and before 'let (data, _) = try await' line of code.
Date and Time, Project Name, and some other info [boringssl] boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
*Note only showed JSON response for a few businesses because of post character limit: {"businesses": [{"id": "hZR-LKgsooHaN6a8L2dprg", "alias": "tako-cheena-orlando", "name": "Tako Cheena", "image_url": "https://s3-media4.fl.yelpcdn.com/bphoto/qj1uAI1X5wJZeSbfCQs17w/o.jpg", "is_closed": false, "url": "https://www.yelp.com/biz/tako-cheena-orlando?adjust_creative=mgN_4fA5wlIrHQMgamUFAQ&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=mgN_4fA5wlIrHQMgamUFAQ", "review_count": 1709, "categories": [{"alias": "asianfusion", "title": "Asian Fusion"}, {"alias": "mexican", "title": "Mexican"}, {"alias": "empanadas", "title": "Empanadas"}], "rating": 4.0, "coordinates": {"latitude": 28.558364, "longitude": -81.3646545}, "transactions": ["pickup", "delivery"], "price": "$", "location": {"address1": "948 N Mills Ave", "address2": "", "address3": "", "city": "Orlando", "zip_code": "32803", "country": "US", "state": "FL", "display_address": ["948 N Mills Ave", "Orlando, FL 32803"]}, "phone": "+14077570626", "display_phone": "(407) 757-0626", "distance": 2650.3293007456186}, {"id": "KlZAG6XPK0GFLAZHkuUPNA", "alias": "wawa-winter-park", "name": "Wawa", "image_url": "https://s3-media1.fl.yelpcdn.com/bphoto/r5-YgTXO0ez_syOSNq9Uhg/o.jpg", "is_closed": false, "url": "https://www.yelp.com/biz/wawa-winter-park?adjust_creative=mgN_4fA5wlIrHQMgamUFAQ&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=mgN_4fA5wlIrHQMgamUFAQ", "review_count": 44, "categories": [{"alias": "servicestations", "title": "Gas Stations"}, {"alias": "coffee", "title": "Coffee & Tea"}, {"alias": "sandwiches", "title": "Sandwiches"}], "rating": 4.5, "coordinates": {"latitude": 28.604943, "longitude": -81.365896}, "transactions": ["pickup", "delivery"], "price": "$", "location": {"address1": "901 N Orlando Ave", "address2": "", "address3": "", "city": "Winter Park", "zip_code": "32789", "country": "US", "state": "FL", "display_address": ["901 N Orlando Ave", "Winter Park, FL 32789"]}, "phone": "+14076290167", "display_phone": "(407) 629-0167", "distance": 7550.802900576834}, 
Print Check: Within repeat-while loop and before 'let businessResults = try JSONDecoder()' line of code.
Error info for catch block in Task block in NewViewController.swift for making API request: keyNotFound(CodingKeys(stringValue: "locationOfRestaurant", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "businesses", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"locationOfRestaurant\", intValue: nil) (\"locationOfRestaurant\").", underlyingError: nil))

The relevant error message:

Error info for catch block in Task block in NewViewController.swift for making API request: keyNotFound(CodingKeys(stringValue: "locationOfRestaurant", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "businesses", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: "locationOfRestaurant", intValue: nil) ("locationOfRestaurant").", underlyingError: nil))

Thanks!


Solution

  • Found out the solution to the problem (thanks to everyone's help!)

    In case anyone wanted to see the final answer:

    The problem was that the property named locationOfRestaurant in the Venue struct was for the location JSON response’s dictionary key, and in order to access it, I had to be using location instead of locationOfRestaurant. When I had used location for the property value for the Venue struct earlier, and used the Location name for the struct that’s currently named LocationOfRestaurant, I got an error because I already had a struct elsewhere in the project already named Location. This is why I had originally changed the Venue struct’s property value name from location” to locationOfRestaurant(I thought the “location” JSON responses’s dictionary key’slocationvalue could still be accessed this way) and theLocationstruct in Venue.swift fromLocationtoLocationOfRestaurant```.

    The solution was changing the property named locationOfRestaurant in the Venue struct to location, and changing that property’s name wherever else in the project it was used, and leaving the name of the struct LocationOfRestaurant as is.

    After making this one change, I had errors for the same basic problem in other parts of my project’s code, and when those were fixed, the table view was shown, and no other errors came up.

    Code before and after change:

    *Note: Also took out the two coding keys in this solution, because they were redundant, as noted by @Rob.

    Before solution change:

    Only code that was eventually changed in Venue.swift:

    *Note: locationOfRestaurant property in the Venue struct below was originally location in the code snippet I had originally posted in the question post, but was using locationOfRestaurant in the code that I was running and using when I made the initial post, and throughout trying to solve the problem when collaborating with others here. Same thing for LocationOfRestaurant struct below; originally used Location in my initial question post, but was actually using LocationOfRestaurant at the time of the post, and when trying to solve the problem when collaborating with others here.

    // MARK: - Business
    struct Venue: Codable {
        let locationOfRestaurant: LocationOfRestaurant
    }
    
    // MARK: - Location
    struct LocationOfRestaurant: Codable {
        let city, country, address2, address3: String?
        let state, address1, zipCode: String?
    }
    

    After solution change:

    Only code that was changed in Venue.swift:

    // MARK: - Business
    struct Venue: Codable {
        let location: LocationOfRestaurant
    }
    
    // MARK: - LocationOfRestaurant
    struct LocationOfRestaurant: Codable {
        let city, country, address2, address3: String?
        let state, address1, zipCode: String?
    }
    

    Thanks for the help everyone!