swiftjsondecoder

API call in swift returning an empty array


I am really new to swift. I am trying an API call, but it just returns an empty array. I am really not sure where I am going wrong. I have been following a couple of tutorials, but it just returns an empty array.

I am not sure if I have defined the model properly or maybe it's going wrong where I am trying to fetch data from the API. I have tried used POSTMAN, to make sure the API is working and it's pulling back results I am trying to pull data from this API here https://docs.trefle.io/docs/advanced/plants-fields#tag/Corrections/operation/createCorrection . I have attached my below code here

import UIKit
import SwiftUI

struct PlantResponse: Codable {
    let data : [Plant]
}

struct Plant: Codable {
    let id: Int
    let common_name: String
    let slug: String
    let scientific_name: String
    let year: Int
    let bibliography: String
    let author: String
    let status: String
    let rank: String
    let family_common_name: String
    let family: String
    let genus_id: Int
    let genus: String
    let image_url: String
    var synonyms = [String]()
    var links = [String]()
}

func fetchPlantsFromAPI() async throws -> [Plant] {
    let url = URL(string: "https://trefle.io/api/v1/plants?token=.[MYTOKEN]&filter[common_name]=beach%20strawberry")!

    let (data, _) = try await URLSession.shared.data(from: url)

    do {
        let decoded = try JSONDecoder().decode(PlantResponse.self, from: data)
        return decoded.data        
    } catch {
        print(error)
        return [Plant]()
    }
}

Task {
    try await fetchPlantsFromAPI()
}

Below is the output I get when I run it on playground

PLAYGROUND OUTPUT

Below is the output I get when I run on POSTMAN

{ "data": [ { "id": 263319, "common_name": "Beach strawberry", "slug": "fragaria-chiloensis", "scientific_name": "Fragaria chiloensis", "year": 1768, "bibliography": "Gard. Dict. ed. 8 : n.° 4 (1768)", "author": "(L.) Mill.", "status": "accepted", "rank": "species", "family_common_name": "Rose family", "genus_id": 12147, "image_url": "https://bs.plantnet.org/image/o/8ee87e6f94833055db1c7df5fc07761852b7b1eb", "synonyms": [ "Fragaria vesca var. chiloensis", "Potentilla chiloensis" ], "genus": "Fragaria", "family": "Rosaceae", "links": { "self": "/api/v1/species/fragaria-chiloensis", "plant": "/api/v1/plants/fragaria-chiloensis", "genus": "/api/v1/genus/fragaria" } } ], "links": { "self": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry", "first": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry&page=1", "last": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry&page=1" }, "meta": { "total": 1 } }

Thanks for your help.


Solution

  • The Plant definition does not match the JSON payload. As a result, JSONDecoder is undoubtedly throwing an error (which was undoubtedly printed to the console). You must look at the console to see what the error was.

    As an aside, I would advise against return [Plant]() (returning an empty array). Instead, I would print the error and then rethrow it with throw error. That way, the caller can catch any errors, and reflect that in the UI accordingly. This is probably why it “returns an empty array”, not because the network response was empty, but rather that there was a decoding error and fetchPlantsFromAPI caught the error, printed it, but then immediately returned an empty array.


    FWIW, when I decoded your JSON, I received the following error:

    typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "links", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))

    That error is telling you that links is a dictionary in the JSON, but not defined as such in your object. Instead one might want to define links in Plant to be a dictionary, [String: String] or [String: URL]. Or, better, given that the keys used in this dictionary are fixed, it would be better to use custom subtype:

    struct Plant: Codable {
        let id: Int
        let commonName: String         // camelCase
        let slug: String
        let scientificName: String     // camelCase
        let year: Int
        let bibliography: String
        let author: String
        let status: String
        let rank: String
        let familyCommonName: String   // camelCase
        let family: String
        let genusId: Int               // camelCase
        let genus: String
        let imageUrl: String           // camelCase
        let synonyms: [String]         // an array of strings
        let links: Links               // a custom type, defined below
    }
    
    extension Plant {
        struct Links: Codable {
            let `self`: String
            let plant: String
            let genus: String
        }
    }
    
    do {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        let decoded = try decoder.decode(PlantResponse.self, from: data)
        print(decoded)
    } catch { 
        print(error)
    }
    

    Note, in addition to redefining links, I also used the standard camelcase property names in Plant, and instructed the JSONDecoder to convert the JSON snakecase keys to camelcase property names.

    But do not get lost in these details: The key observation is that you should look at the Error that was thrown to diagnose what went wrong.


    Just so we can more clearly read the JSON, here it is in “pretty” format:

    {
        "data": [
            {
                "id": 263319,
                "common_name": "Beach strawberry",
                "slug": "fragaria-chiloensis",
                "scientific_name": "Fragaria chiloensis",
                "year": 1768,
                "bibliography": "Gard. Dict. ed. 8 : n.° 4 (1768)",
                "author": "(L.) Mill.",
                "status": "accepted",
                "rank": "species",
                "family_common_name": "Rose family",
                "genus_id": 12147,
                "image_url": "https://bs.plantnet.org/image/o/8ee87e6f94833055db1c7df5fc07761852b7b1eb",
                "synonyms": [
                    "Fragaria vesca var. chiloensis",
                    "Potentilla chiloensis"
                ],
                "genus": "Fragaria",
                "family": "Rosaceae",
                "links": {
                    "self": "/api/v1/species/fragaria-chiloensis",
                    "plant": "/api/v1/plants/fragaria-chiloensis",
                    "genus": "/api/v1/genus/fragaria"
                }
            }
        ],
        "links": {
            "self": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry",
            "first": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry&page=1",
            "last": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry&page=1"
        },
        "meta": {
            "total": 1
        }
    }
    

    FWIW, I use https://jsonlint.com to check the JSON and render it in a pretty format.

    Again, the links in this JSON is a dictionary which can be decoded as its own type.