EN VI

Swift - How do I return JSON data?

2024-03-16 00:30:06
Swift - How do I return JSON data?

I am trying to return JSON data but I have no clue on how to do it. I get this error message "Cannot convert return expression of type '[Plant]' to return type 'Plant'". I sort of understand this error message, it means I can't convert one type to another type. But not sure how to solve this issue. Please see my code below:

import UIKit
import SwiftUI

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

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
    let Plant: [Plant]
}

extension Plant {
    struct Links: Codable {
        let `self`: String
        let plant: String
        let genus: String
    }
}

struct ContentView: View {
    @State var plant: Plant?
    
    var body: some View {
        VStack(alignment: .leading) {
            if let plant {
                Text("There is data")
                    .font(.title)
            } else {
                Text("No data available")
            }
        }
        .padding(20.0)
        .task {
            do {
                plant = try await fetchPlantsFromAPI()
            } catch {
                plant = nil
            }
        }
    }
}

func fetchPlantsFromAPI() async throws -> Plant {
    let url = URL(string: "https://trefle.io/api/v1/plants? token=d321f518jTdU1t-doZQif3jpzzW9V0mk3nLnDssF1vY&filter[common_name]=beach%20strawberry")!
    do {
   
        let (data, _) = try await URLSession.shared.data(from: url)

        let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
        let decoded = try! decoder.decode(PlantResponse.self, from: data)
        print(decoded.data)
        return decoded.data //Cannot convert return expression of type '[Plant]' to return type 'Plant'
    } catch {
        throw error
    }
}

#Preview {
    ContentView()
}

Any help would be appreciated.

Solution:

The error is pointing out that the API returns an array of Plant, i.e. [Plant], so change the method signature accordingly:

func fetchPlantsFromAPI() async throws -> [Plant] {…}

Then change the @State variable to be an array:

struct ContentView: View {
    @State var plants: [Plant] = []

    var body: some View {
        List(plants) { plant in
            Text(plant.commonName)
        }
        .padding(20.0)
        .task {
            do {
                plants = try await fetchPlantsFromAPI()
            } catch {
                print(error)
                plants = []
            }
        }
    }
}

And, I would declare Plant to be Identifiable and Sendable:

struct Plant: Codable, Identifiable, Sendable {…}

Testing this, I see a few additional issues:

  1. I would use URLComponents to make sure the query is properly escaped:

    func fetchPlantsFromAPI() async throws -> [Plant] {
        guard var components = URLComponents(string: "https://trefle.io/api/v1/plants") else {
            throw URLError(.badURL)
        }
    
        components.queryItems = [
            URLQueryItem(name: "token", value: "…"),
            URLQueryItem(name: "filter[common_name]", value: "beach strawberry")
        ]
    
        guard let url = components.url else {
            throw URLError(.badURL)
        }
    
        let (data, _) = try await URLSession.shared.data(from: url)
    
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        let decoded = try decoder.decode(PlantResponse.self, from: data)
        return decoded.data
    }
    
  2. I would avoid try!. As you see in my revised example above, try is sufficient. You do not want your app crashing if there is a coding error.

  3. There is no point in a do-try-catch if all you are doing in the catch is rethrowing the error. Again, I have removed that from point 1, above.

  4. You have added a Plant property to Plant type. You should remove that:

    struct Plant: Codable, Identifiable, Sendable {
        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
    //    let Plant: [Plant]
    }
    

    If we look at the JSON, there is no Plant key within the array of plants:

    {
        "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\u0026page=1",
            "last": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry\u0026page=1"
        },
        "meta": {
            "total": 1
        }
    }
    
  5. I would decode API errors, so we can handle them gracefully. E.g.,

    struct ErrorResponse: Codable {
        let error: Bool
        let messages: String
    }
    
    enum PlantError: Error {
        case apiError(String)
    }
    
    func fetchPlantsFromAPI() async throws -> [Plant] {
        guard var components = URLComponents(string: "https://trefle.io/api/v1/plants") else {
            throw URLError(.badURL)
        }
    
        components.queryItems = [
            URLQueryItem(name: "token", value: "…"),
            URLQueryItem(name: "filter[common_name]", value: "beach strawberry")
        ]
    
        guard let url = components.url else {
            throw URLError(.badURL)
        }
    
        let (data, response) = try await URLSession.shared.data(from: url)
    
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
    
        guard
            let httpResponse = response as? HTTPURLResponse,
            200...299 ~= httpResponse.statusCode
        else {
            let errorObject = try decoder.decode(ErrorResponse.self, from: data)
            throw PlantError.apiError(errorObject.messages)
        }
    
        return try decoder
            .decode(PlantResponse.self, from: data)
            .data
    }
    
Answer

Login


Forgot Your Password?

Create Account


Lost your password? Please enter your email address. You will receive a link to create a new password.

Reset Password

Back to login