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.
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:
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
}
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.
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.
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
}
}
I would decode API errors, so we can handle them gracefully. E.g.,
struct FailureResponse: Codable {
let error: Bool
let messages: String
}
enum PlantError: Error {
case apiError(Int, String)
case invalidResponse(URLResponse)
}
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 else {
throw PlantError.invalidResponse(response)
}
guard 200...299 ~= httpResponse.statusCode else {
let errorObject = try decoder.decode(FailureResponse.self, from: data)
throw PlantError.apiError(httpResponse.statusCode, errorObject.messages)
}
return try decoder
.decode(PlantResponse.self, from: data)
.data
}
FWIW, I would be inclined to make the API code generic, so it can work with any of the endpoints (so we do not have to repeat this code all over the place). Perhaps:
struct SuccessResponse<T: Decodable>: Decodable {
let data: T
}
struct FailureResponse: Decodable {
let error: Bool
let messages: String
}
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: Plant.Links // a custom type, defined below
}
extension Plant {
struct Links: Codable {
let `self`: String
let plant: String
let genus: String
}
}
enum ApiError: Error {
case apiError(Int, String)
case invalidResponse(URLResponse)
}
func urlForPlantSearch(_ string: String) throws -> URL {
guard var components = URLComponents(string: "https://trefle.io/api/v1/plants") else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(name: "token", value: apiToken),
URLQueryItem(name: "filter[common_name]", value: string)
]
guard let url = components.url else {
throw URLError(.badURL)
}
return url
}
func fetchPlantsFromAPI() async throws -> [Plant] {
let url = try urlForPlantSearch("beach strawberry")
return try await fetchFromAPI(url: url)
}
// Make this generic so we can reuse this code with any query
private func fetchFromAPI<T: Decodable>(url: URL) async throws -> T {
let (data, response) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let httpResponse = response as? HTTPURLResponse else {
throw ApiError.invalidResponse(response)
}
guard 200...299 ~= httpResponse.statusCode else {
let errorObject = try decoder.decode(FailureResponse.self, from: data)
throw ApiError.apiError(httpResponse.statusCode, errorObject.messages)
}
return try decoder
.decode(SuccessResponse<T>.self, from: data)
.data
}