Search code examples
swiftswiftui

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 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
      }
      
    6. 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
      }