Search code examples
jsonswiftapialamofiredecodable

Why is my decoder not able to decode my api call with Alamofire in swift


So I'm trying to to process my API call data into an object I'm calling PlaceModel.

The API returns a lot of data; but I only need name, and coordinate data for my purposes at the moment.

example of API data

--- redacted for brevity ---
{
   "html_attributions" : [],
   "next_page_token" : "AeJbb3dLZZ4oYIangr8wO9j6SE4_5L1NcYQC-qCKt1Y0saPFOzNc90E7bG8IDtj8DNlEeM6nWlClZgFd-YMtB4kGQYBLexZgjlX-nH9rN4UQHk9hk-Ha9zLpjY0GYiUsayMZKW5x7oAkQOXlmbARFxu2tR-dW5VB_dd0VkJkf7_6zeGeeje0UjQeqdk1Czr-gBgNxzmMj0adrj2_G0CxeJxk5eN645sdbZ8QJ-BChqkUegJ8l9V7dO4nRCfPz5u_paqoCEcVOJqYMyiZsc3ibBxNowY-uH2PCbpyXcM186CenqBIG088TiyulBFL4Dq1zDxXbZ5asSH4r-u8UtWpdpEcpzAJMjIT4W64vvXYZUT3_918VgENIyCEkPQVebjZUF0X2USykC-ZdlsB2BMvxjKlwx5Q_7_yKYy3Gnxq6T6jTgGisAvNXQ",
   "results" : [
      {
         "business_status" : "OPERATIONAL",     // FIRST BUSINESS
         "geometry" : {
            "location" : {
               "lat" : 49.24665,
               "lng" : -122.7504486    // HERE IS SOME LOCATION INFO
            },
            "viewport" : {
               "northeast" : {
                  "lat" : 49.24808953029149,
                  "lng" : -122.7490256697085
               },
               "southwest" : {
                  "lat" : 49.2453915697085,
                  "lng" : -122.7517236302915
               }
            }
         },
         "icon" : "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/school-71.png",
         "icon_background_color" : "#7B9EB0",
         "icon_mask_base_uri" : "https://maps.gstatic.com/mapfiles/place_api/icons/v2/school_pinlet",
         "name" : "Ascension Martial Arts",   // HERE IS A NAME
         "photos" : [
            {
               "height" : 1668,
               "html_attributions" : [
                  "\u003ca href=\"https://maps.google.com/maps/contrib/115539551484221381565\"\u003eAscension Martial Arts\u003c/a\u003e"
               ],
               "photo_reference" : "AeJbb3dunz49PLKAwwhcbLid5d2yWHftgLOY5sdMPLXhCKohiOcfvinKkmFxIht8ZBSNgxTFVJCCbnHxOG7ROlt093iHntMlX4O80ihQ0MI80u7u_TvvKMcwu3os0tgfr84KaNVP1KzNrzTiS1GUF0S82OM43NEuWoMjBW5o2PmhH-Ke-tj4",
               "width" : 2500
            }
         ],
         "place_id" : "ChIJST1805B2hlQRASo6CA1ejKw",
         "plus_code" : {
            "compound_code" : "66WX+MR Port Coquitlam, BC, Canada",
            "global_code" : "84XV66WX+MR"
         },
         "rating" : 4.9,
         "reference" : "ChIJST1805B2hlQRASo6CA1ejKw",
         "scope" : "GOOGLE",
         "types" : [ "gym", "health", "point_of_interest", "establishment" ],
         "user_ratings_total" : 107,
         "vicinity" : "109-1320 Kingsway Avenue, Port Coquitlam"
      },
      {
         "business_status" : "OPERATIONAL",      // NEXT BUSINESS

--- redacted for brevity ---

So I really only need those 2 pieces of data for marking spots on my map.

Now I'm just trying to debug it and get it to work making Struct objects but I'm stuck.

networkingLayerAF.swift

import Foundation
import Alamofire

class NetworkingLayerAF{
    
    func getPlaces() {
                
        let longitudeX = "-122.735960"
        let latitudeY = "49.252400"
        
        let url = URL(string:"https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(latitudeY),\(longitudeX)&radius=150000&types=gym&key=NFS")!
     
//        AF.request(url).response{ response in
//            debugPrint(response)
//        }
        
        print("LAUNCH ALAMO")
        AF.request(url).responseDecodable(of: [PlaceModel].self){ response in
            switch response.result {
            case .success(let items):
                print(items)
                
            case .failure(let error):
                print(error.localizedDescription)
            }
        }  
    }//
}// end class

Error I'm getting when it runs in console.

LAUNCH ALAMO
2022-07-31 17:06:24.217509-0700 MyAPP[14661:1355709] [boringssl] boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
Response could not be decoded because of error:
The data couldn’t be read because it isn’t in the correct format.

PlaceModel

import Foundation

struct PlaceModel: Decodable {
    var name: String
    var coordinate: Coordinate
    
    enum CodingKeys: String, CodingKey {
        case name = "name"
        case coordinate
    }
}

struct Coordinate {
    var latitude: Double
    var longitude: Double

    enum CodingKeys: String, CodingKey {
        case latitude
        case longitude
    }
}

extension Coordinate: Decodable {
    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: Coordinate.CodingKeys.self)
        latitude = try values.decode(Double.self, forKey: .latitude)
        longitude = try values.decode(Double.self, forKey: .longitude)
    }
}

Earlier I was trying to use this websites json to swift converter which gave me a different looking struct class

placeModel - I added quite a few decodable tags onto the converter's code.

import Foundation
import CoreLocation

struct Welcome1: Decodable{
        let nextPageToken: String
        let results: [Result]
        let status: String
    }

    // MARK: - Result
struct Result : Decodable{
        let businessStatus: BusinessStatus
        let geometry: Geometry
        let icon: String
        let iconBackgroundColor: IconBackgroundColor
        let iconMaskBaseURI: String
        let name: String
        let photos: [Photo]?
        let placeID: String
        let plusCode: PlusCode
        let rating: Double?
        let reference: String
        let scope: Scope
        let types: [TypeElement]
        let userRatingsTotal: Int?
        let vicinity: String
        let openingHours: OpeningHours?
    }

enum BusinessStatus: CodingKey , Decodable{
        case operational
    }

    // MARK: - Geometry
struct Geometry : Decodable {
        let location: Location
        let viewport: Viewport
    }

    // MARK: - Location
struct Location : Decodable{
        let lat, lng: Double
    }

    // MARK: - Viewport
struct Viewport : Decodable{
        let northeast, southwest: Location
    }

    enum IconBackgroundColor: CodingKey, Decodable {
        case the7B9Eb0
    }

    // MARK: - OpeningHours
struct OpeningHours : Decodable{
        let openNow: Bool
    }

    // MARK: - Photo
struct Photo : Decodable{
        let height: Int
        let htmlAttributions: [String]
        let photoReference: String
        let width: Int
    }

    // MARK: - PlusCode
struct PlusCode : Decodable{
        let compoundCode, globalCode: String
    }

    enum Scope: CodingKey , Decodable{
        case google
    }

    enum TypeElement: CodingKey , Decodable {
        case establishment
        case gym
        case health
        case pointOfInterest
        case school
    }


But I kept getting pretty much the same error

LAUNCH ALAMO
Response could not be decoded because of error:
The data couldn’t be read because it isn’t in the correct format.

One thing I will note; when I use print(error) instead of LocalizedDescription I get this error:

LAUNCH ALAMO
2022-07-31 17:33:09.489244-0700 Body Fat Calculator[15033:1371997] [boringssl] boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
responseSerializationFailed(reason: Alamofire.AFError.ResponseSerializationFailureReason.decodingFailed(error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))))

So I edited the code a little to: AF.request(url).responseDecodable(of: Welcome1.self) // Not [Welcome1] now.

and I'm getting some new errors

Welcome1

LAUNCH ALAMO
responseSerializationFailed(reason: Alamofire.AFError.ResponseSerializationFailureReason.decodingFailed(error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "nextPageToken", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"nextPageToken\", intValue: nil) (\"nextPageToken\").", underlyingError: nil))))

PlaceModel

LAUNCH ALAMO
2022-07-31 17:43:32.518882-0700 Body Fat Calculator[15209:1379339] [boringssl] boringssl_metrics_log_metric_block_invoke(153) Failed to log metrics
responseSerializationFailed(reason: Alamofire.AFError.ResponseSerializationFailureReason.decodingFailed(error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil))))

Ok this getting a bit long, I'm going to keep trying stuff any insights appreciated happy to provide more info if needed :)

Thank you :D


Solution

  • this example code does not use Alamofire, but should give you enough to get your data decoded and extract the name, and coordinate as per your question.

    Since I do not have your key, I included the json you show as a simulated response.

    The crux here is to match your struct models to the JSON data, otherwise you cannot decode it without errors. Works well for me.

    You will need to find from the server docs, which fields are optional and adjust the various struct models accordingly.

    import Foundation
    import SwiftUI
    
    struct ContentView: View {
        @StateObject var viewModel = BusinessService()
        
        var body: some View {
            List (viewModel.businessList) { bisnes in
                VStack {
                    Text(bisnes.name)
                    Text("lat: \(bisnes.geometry.location.lat)")
                    Text("lon: \(bisnes.geometry.location.lng)")
                }
            }
            .task {
                do {
                    try await viewModel.getData(lat: -122.735960, lon: 49.252400)
                } catch{
                    print("---> ContentView error: \(error)")
                }
            }
        }
    }
    
    class BusinessService: ObservableObject{
        @Published var businessList: [Business] = []
        
        let nfs = "xxxxxxx"  // <--- here your key
        
        func getData2(lat: Double, lon: Double) async throws {
            if let url = URL(string: "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(lat),\(lon)&radius=150000&types=gym&key=\(nfs)") {
                let (data, _) = try await URLSession.shared.data(from: url)
                Task{@MainActor in
                    let responseData = try JSONDecoder().decode(ServerResponse.self, from: data)
                    self.businessList = responseData.results
                    //  print(businessList)  // <-- for testing
                }
            }
        }
        
        // simulated
        func getData(lat: Double, lon: Double) async throws {
            let json = """
     {
        "html_attributions" : [],
        "next_page_token" : "AeJbb3dLZZ4oYIangr8wO9j6SE4_5L1NcYQC-qCKt1Y0saPFOzNc90E7bG8IDtj8DNlEeM6nWlClZgFd-YMtB4kGQYBLexZgjlX-nH9rN4UQHk9hk-Ha9zLpjY0GYiUsayMZKW5x7oAkQOXlmbARFxu2tR-dW5VB_dd0VkJkf7_6zeGeeje0UjQeqdk1Czr-gBgNxzmMj0adrj2_G0CxeJxk5eN645sdbZ8QJ-BChqkUegJ8l9V7dO4nRCfPz5u_paqoCEcVOJqYMyiZsc3ibBxNowY-uH2PCbpyXcM186CenqBIG088TiyulBFL4Dq1zDxXbZ5asSH4r-u8UtWpdpEcpzAJMjIT4W64vvXYZUT3_918VgENIyCEkPQVebjZUF0X2USykC-ZdlsB2BMvxjKlwx5Q_7_yKYy3Gnxq6T6jTgGisAvNXQ",
        "results" : [
           {
              "business_status" : "OPERATIONAL",
             "geometry" : {
                 "location" : {
                    "lat" : 49.24665,
                    "lng" : -122.7504486
                 },
                 "viewport" : {
                    "northeast" : {
                       "lat" : 49.24808953029149,
                       "lng" : -122.7490256697085
                    },
                    "southwest" : {
                       "lat" : 49.2453915697085,
                       "lng" : -122.7517236302915
                    }
                 }
              },
              "icon" : "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/school-71.png",
              "icon_background_color" : "#7B9EB0",
              "icon_mask_base_uri" : "https://maps.gstatic.com/mapfiles/place_api/icons/v2/school_pinlet",
              "name" : "Ascension Martial Arts",
              "photos" : [
                 {
                    "height" : 1668,
                    "html_attributions" : ["Arts"],
                    "photo_reference" : "AeJbb3dunz49PLKAwwhcbLid5d2yWHftgLOY5sdMPLXhCKohiOcfvinKkmFxIht8ZBSNgxTFVJCCbnHxOG7ROlt093iHntMlX4O80ihQ0MI80u7u_TvvKMcwu3os0tgfr84KaNVP1KzNrzTiS1GUF0S82OM43NEuWoMjBW5o2PmhH-Ke-tj4",
                    "width" : 2500
                 }
              ],
              "place_id" : "ChIJST1805B2hlQRASo6CA1ejKw",
              "plus_code" : {
                 "compound_code" : "66WX+MR Port Coquitlam, BC, Canada",
                 "global_code" : "84XV66WX+MR"
              },
              "rating" : 4.9,
              "reference" : "ChIJST1805B2hlQRASo6CA1ejKw",
              "scope" : "GOOGLE",
              "types" : [ "gym", "health", "point_of_interest", "establishment" ],
              "user_ratings_total" : 107,
              "vicinity" : "109-1320 Kingsway Avenue, Port Coquitlam"
           }
     ]
     }
    """
            let data = json.data(using: .utf8)!
            Task{@MainActor in
                let responseData = try JSONDecoder().decode(ServerResponse.self, from: data)
                self.businessList = responseData.results
                print(businessList)
            }
            
        }
        
    }
    
    struct ServerResponse: Codable {
        let htmlAttributions: [String]
        let nextPageToken: String
        let results: [Business]
        
        enum CodingKeys: String, CodingKey {
            case htmlAttributions = "html_attributions"
            case nextPageToken = "next_page_token"
            case results
        }
    }
    
    // MARK: - Business
    struct Business: Codable, Identifiable {
        let id = UUID()
        let businessStatus: String
        let geometry: Geometry
        let icon: String
        let iconBackgroundColor: String
        let iconMaskBaseURI: String
        let name: String
        let photos: [Photo]?
        let placeID: String
        let plusCode: PlusCode
        let rating: Double?
        let reference, scope: String
        let types: [String]
        let userRatingsTotal: Int?
        let vicinity: String
        
        enum CodingKeys: String, CodingKey {
            case businessStatus = "business_status"
            case geometry, icon
            case iconBackgroundColor = "icon_background_color"
            case iconMaskBaseURI = "icon_mask_base_uri"
            case name, photos
            case placeID = "place_id"
            case plusCode = "plus_code"
            case rating, reference, scope, types
            case userRatingsTotal = "user_ratings_total"
            case vicinity
        }
    }
    
    // MARK: - Geometry
    struct Geometry: Codable {
        let location: Location
        let viewport: Viewport
    }
    
    // MARK: - Location
    struct Location: Codable {
        let lat, lng: Double
    }
    
    // MARK: - Viewport
    struct Viewport: Codable {
        let northeast, southwest: Location
    }
    
    // MARK: - Photo
    struct Photo: Codable {
        let height: Int
        let htmlAttributions: [String]
        let photoReference: String
        let width: Int
        
        enum CodingKeys: String, CodingKey {
            case height
            case htmlAttributions = "html_attributions"
            case photoReference = "photo_reference"
            case width
        }
    }
    
    // MARK: - PlusCode
    struct PlusCode: Codable {
        let compoundCode, globalCode: String
        
        enum CodingKeys: String, CodingKey {
            case compoundCode = "compound_code"
            case globalCode = "global_code"
        }
    }
    

    Using https://app.quicktype.io/ as you probably did, is a good idea. However, you need to adjust what it gives you. For example, best not to use the word Result (because Swift already has this struct declared), I used Business.