JSON decoding challenges with Combine

I am using a Combine URLSession to pull data from the Food Data Central API and the Data I am receiving is not being decoded with the JSON decoder. I know the Data is being received because I use the String(data:encoding: .utf8) as a debug print and I can see the downloaded data correctly in the console. I get an error message after the .decode completion failure that says "The data couldn't be read because it isn't in the correct format."

I am guessing I have to add something like the "encoder: utf8" statement in the .decode function. Or maybe transform the data in the .tryMap closure before returning. But I have searched the documentation and other sources and have not found anywhere that discusses this.

I am a fairly new to Swift (my first real app), I am hoping someone more-experienced can point me in the right direction. My code is as follows:

private func fdcSearch(searchFor searchText: String) {

        let query = "https://api.nal.usda.gov/fdc/v1/foods/search?api_key=***&query=+Apple%20+Fuji"

        let searchURL = "https://api.nal.usda.gov/fdc/v1/foods/search?"

        let searchQuery = "&query="+searchString

        print(searchURL+devData.apiKey+searchQuery)

//        guard let url = URL(string: "https://api.nal.usda.gov/fdc/v1/foods/search?api_key=***&query=AppleFuji") else {

        guard let url = URL(string: searchURL+devData.apiKey+searchQuery) else {

            print("Guard error on url assignment") // debug statement

            return

        }

        print("In fdcSearch") // debug statement

        fdcSearchSubscription = URLSession.shared.dataTaskPublisher(for: url)

            .subscribe(on: DispatchQueue.global(qos: .default))

            .tryMap { (output) -> Data in

                guard let response = output.response as? HTTPURLResponse, response.statusCode >= 200 && response.statusCode < 300 else {

                    print("bad server response") // debug statement

                    throw URLError(.badServerResponse)

                }

                print("got output") // debug statement

                if let dataString = String(data: output.data, encoding: .utf8) { // debug statement

                        print("got dataString: \n\(dataString)") // debug statement

                    } // debug statement

                return output.data

            }

            .receive(on: DispatchQueue.main)

            .decode(type: [FDCFoodItem].self, decoder: JSONDecoder())

            .sink { (completion) in

                switch completion {

                case .finished:

                    print("Completion finished") // debug statement

                    break

                case .failure(let error):

                    print("Completion failed") // debug statement

                    print(error.localizedDescription)

                }

            } receiveValue: { [weak self] (returnedFoods) in

                self?.foods = returnedFoods

                print("returnedFoods: \(returnedFoods)") // debug statement

                print("self?.foods: \(String(describing: self?.foods))") // debug statement

            }

    }

Any suggestions on how to handle this?

I can see the downloaded data correctly in the console

Can you show the example of the downloaded data?


"The data couldn't be read because it isn't in the correct format."

The error message is shown when the data type (in your case, [FDCFoodItem]) and the actual data returned mismatch. Please show the definition of FDCFoodItem.

But you should use print(error) instead of print(error.localizedDescription). print(error) will show you more info about what is wrong.

Please show the full message you can get with print(error).


I am guessing I have to add something like the "encoder: utf8" statement in the .decode function

Bad guess. There's no such parameter like encoder: utf8. All raw response in JSON should be encoded in UTF-8 and you have no need to specify encodings when using JSONDecoder.

Or maybe transform the data in the .tryMap closure before returning. 

Again, very unlikely. .decode would do the transform in this case.


Anyway, if you could show enough info as above, you would be able to find the right solution.

The data I am getting from the call is:



{"totalHits":146,"currentPage":1,"totalPages":3,"pageList":[1,2,3],"foodSearchCriteria":{"query":"+Apple +Fuji","generalSearchInput":"+Apple +Fuji","pageNumber":1,"numberOfResultsPerPage":50,"pageSize":50,"requireAllWords":false},"foods":[{"fdcId":1750340,"description":"Apples, fuji, with skin, raw","lowercaseDescription":"apples, fuji, with skin, raw","scientificName":"Malus domestica","commonNames":"","additionalDescriptions":"","dataType":"Foundation","ndbNumber":9504,"publishedDate":"2020-10-30","foodCategory":"Fruits and Fruit Juices","mostRecentAcquisitionDate":"2020-05-05","allHighlightFields":"","score":698.1876,"foodNutrients":[{"nutrientId":1089,"nutrientName":"Iron, Fe","nutrientNumber":"303","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":0.015},{"nutrientId":1090,"nutrientName":"Magnesium, Mg","nutrientNumber":"304","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":4.67},{"nutrientId":1091,"nutrientName":"Phosphorus, P","nutrientNumber":"305","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":9.78},{"nutrientId":1092,"nutrientName":"Potassium, K","nutrientNumber":"306","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":104},{"nutrientId":1093,"nutrientName":"Sodium, Na","nutrientNumber":"307","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":1.01},{"nutrientId":1095,"nutrientName":"Zinc, Zn","nutrientNumber":"309","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":0.0168},{"nutrientId":1098,"nutrientName":"Copper, Cu","nutrientNumber":"312","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":0.0328},{"nutrientId":1101,"nutrientName":"Manganese, Mn","nutrientNumber":"315","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":0.0326},{"nutrientId":1165,"nutrientName":"Thiamin","nutrientNumber":"404","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":0.00625},{"nutrientId":1166,"nutrientName":"Riboflavin","nutrientNumber":"405","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":0.0675},{"nutrientId":1167,"nutrientName":"Niacin","nutrientNumber":"406","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":0.0906},{"nutrientId":2000,"nutrientName":"Sugars, total including NLEA","nutrientNumber":"269","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":13.3},{"nutrientId":1175,"nutrientName":"Vitamin B-6","nutrientNumber":"415","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":0.0354},{"nutrientId":1177,"nutrientName":"Folate, total","nutrientNumber":"417","unitName":"UG","derivationCode":"A","derivationDescription":"Analytical","value":0.0},{"nutrientId":1051,"nutrientName":"Water","nutrientNumber":"255","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":83.6},{"nutrientId":1002,"nutrientName":"Nitrogen","nutrientNumber":"202","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":0.0238},{"nutrientId":1004,"nutrientName":"Total lipid (fat)","nutrientNumber":"204","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":0.162},{"nutrientId":1007,"nutrientName":"Ash","nutrientNumber":"207","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":0.428},{"nutrientId":1010,"nutrientName":"Sucrose","nutrientNumber":"210","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":1.7},{"nutrientId":1011,"nutrientName":"Glucose (dextrose)","nutrientNumber":"211","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":3.04},{"nutrientId":1012,"nutrientName":"Fructose","nutrientNumber":"212","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":8.59},{"nutrientId":1013,"nutrientName":"Lactose","nutrientNumber":"213","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":0.0},{"nutrientId":1014,"nutrientName":"Maltose","nutrientNumber":"214","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":0.0},{"nutrientId":1079,"nutrientName":"Fiber, total dietary","nutrientNumber":"291","unitName":"G","derivationCode":"A","derivationDescription":"Analytical","value":2.08},{"nutrientId":1087,"nutrientName":"Calcium, Ca","nutrientNumber":"301","unitName":"MG","derivationCode":"A","derivationDescription":"Analytical","value":5.98},{"nutrientId":1003,"nutrientName":"Protein","nutrientNumber":"203","unitName":"G","derivationCode":"NC","derivationDescription":"Calculated","value":0.148},{"nutrientId":1005,"nutrientName":"Carbohydrate, by difference","nutrientNumber":"205","unitName":"G","derivationCode":"NC","derivationDescription":"Calculated","value":15.7},{"nutrientId":2047,"nutrientName":"Energy (Atwater General Factors)","nutrientNumber":"957","unitName":"KCAL","derivationCode":"NC","derivationDescription":"Calculated","value":64.7},{"nutrientId":2048,"nutrientName":"Energy (Atwater Specific Factors)","nutrientNumber":"958","unitName":"KCAL","derivationCode":"NC","derivationDescription":"Calculated","value":58.2},{"nutrientId":1050,"nutrientName":"Carbohydrate, by summation","nutrientNumber":"205.2","unitName":"G","derivationCode":"AS","derivationDescription":"Summed","value":15.4},{"nutrientId":1063,"nutrientName":"Sugars, Total NLEA","nutrientNumber":"269.3","unitName":"G","derivationCode":"AS","derivationDescription":"Summed","value":13.3}]},{"fdcId":167793

and the FoodItem structure is:

struct FDCFoodItem: Codable {

    var fdcId: Int

    var description: String

    var dataType: String

    var gtinUpc: String?

    var brandName: String?

    var foodCategory: String

    var score: Double

    var foodNutrients: [FoodNutrient]


}



extension FDCFoodItem: Identifiable {

    var id: Int { return fdcId }

}

    

struct FoodNutrient: Codable {

    var nutrientId: Int

    var nutrientName: String

    var nutrientNumber: String

    var unitName: String

    var derivationCode: DerivationCode?

    var derivationDescription: String?

    var value: Double

}

When I print(error), I get the following response:

typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))

I checked the declaration of the foods array and it looks right.

class FDCDataService {

    

    @Published var foods: Array<FDCFoodItem> = []

    var cancellables = Set<AnyCancellable>()

    var fdcSearchSubscription: AnyCancellable?

    

    let devData: Constants = Constants()

    let searchString = "%2BApple%20%2BFuji"

    

    init() {

        print("In class FDCDataService init()")

        fdcSearch(searchFor: searchString)

        print("After fdcSearch() cll")

    }
    

    private func fdcSearch(searchFor searchText: String) { [CODE CONTINUES AS SHOWN ABOVE]

OK, after a little research, I see that the JSON response includes some summary info about the request and is started with a curly bracket (the "dictionary" indicator.) So is there a way to specify not to include that data and just return the foods? Or is the right answer to create a add a dictionary property to my class and have it include the summary info variables as well as the [FoodItems] as an element of the dictionary?

OK, after a little research, I see that the JSON response includes some summary info about the request and is started with a curly bracket (the "dictionary" indicator.) So is there a way to specify not to include that data and just return the foods? Or is the right answer to create a add a dictionary property to my class and have it include the summary info variables as well as the [FoodItems] as an element of the dictionary?

Thanks for showing the data, the definition and the error info.

You may need another struct containing some summary info about the request:

struct FoodSearchResult: Codable {
    let totalHits, currentPage, totalPages: Int
    let pageList: [Int]
    let foodSearchCriteria: FoodSearchCriteria
    let foods: [FDCFoodItem]
}

struct FoodSearchCriteria: Codable {
    let query, generalSearchInput: String
    let pageNumber, numberOfResultsPerPage, pageSize: Int
    let requireAllWords: Bool
}

(Assuming your FDCFoodItem is really correct.)

And pass it to .decode:

            .decode(type: FoodSearchResult.self, decoder: JSONDecoder())

You may also need to modify the closure passed to receiveValue::

            } receiveValue: { [weak self] result in
                self?.foods = result.foods
                print("result: \(result)") // debug statement
                print("self?.foods: \(String(describing: self?.foods))") // debug statement
            }
JSON decoding challenges with Combine
 
 
Q