Swift: getting nil when decoding API response

303 views Asked by At

I'm having an issue decoding an API response.

So we have a NetworkManager class which we use to decode APIs. I have a simple GET endpoint that I need to retrieve a list of airports from. Here is the endpoint:

static let airports = Endpoint(url: "/test/airports")

Endpoint is defined as follows:

public struct Endpoint : Equatable {
    public init(url: String? = nil, pattern: String? = nil, methods: [Test.HTTPMethod] = [.get], type: Test.EncodingType = .json)
}

Then in our network manager we have:

   public func call<R: Decodable>(_ endpoint: Endpoint,
                        with args: [String: String]? = nil,
                        using method: HTTPMethod = .get,
                        expecting response: R.Type?,
                        completion: APIResponse<R>) {
    call(endpoint, with: args, parameters: Nothing(),
         using: method, posting: Nothing(), expecting: response, completion: completion)
}

My Airport model is as follows:

struct Airport: Codable {
    let id: String
    let name: String
    let iata3: String
    let icao4: String
    let countryCode: String
}

And then I'm calling the endpoint like:

private func getAirportsList() {
    API.client.call(.airports, expecting: [Airport].self) { (result, airports) in
        print(airports)
    }
}

Now I'm using Charles to proxy and I am getting the response I expect:

[{
    "id": "5f92b0269c983567fc4b9683",
    "name": "Amsterdam Schiphol",
    "iata3": "AMS",
    "icao4": "EHAM",
    "countryCode": "NL"
}, {
    "id": "5f92b0269c983567fc4b9685",
    "name": "Bahrain International",
    "iata3": "BAH",
    "icao4": "OBBI",
    "countryCode": "BH"
}, {
    "id": "5f92b0269c983567fc4b968b",
    "name": "Bankstown",
    "iata3": "BWU",
    "icao4": "YSBK",
    "countryCode": "AU"
}]

But in my getAirports() method, airports is nil. I'm really struggling to see why. Clearly the endpoint is being hit correctly but my decoding is failing.

Edit:

Full method:

private func call<P: Encodable, B: Encodable, R: Decodable>(_ endpoint: Endpoint,
                                                                with args: [String: String]? = nil,
                                                                parameters params: P?,
                                                                using method: HTTPMethod = .get,
                                                                posting body: B?,
                                                                expecting responseType: R.Type?,
                                                                completion: APIResponse<R>) {

        // Prepare our URL components

        guard var urlComponents = URLComponents(string: baseURL.absoluteString) else {
            completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
            return
        }

        guard let endpointPath = endpoint.url(with: args) else {
            completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
            return
        }

        urlComponents.path = urlComponents.path.appending(endpointPath)

        // Apply our parameters

        applyParameters: if let parameters = try? params.asDictionary() {
            if parameters.count == 0 {
                break applyParameters
            }

            var queryItems = [URLQueryItem]()

            for (key, value) in parameters {
                if let value = value as? String {
                    let queryItem = URLQueryItem(name: key, value: value)
                    queryItems.append(queryItem)
                }
            }

            urlComponents.queryItems = queryItems
        }

        // Try to build the URL, bad request if we can't

        guard let urlString = urlComponents.url?.absoluteString.removingPercentEncoding,
            var url = URL(string: urlString) else {
                completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
                return
        }
        
        if let uuid = UIDevice.current.identifierForVendor?.uuidString, endpoint.pattern == "/logging/v1/device/<device_id>" {
            let us = "http://192.168.6.128:3000/logging/v1/device/\(uuid)"
            guard let u = URL(string: us) else { return }
            url = u
        }

        // Can we call this method on this endpoint? If not, lets not try to continue

        guard endpoint.httpMethods.contains(method) else {
            completion?(.failure(nil, NetworkError(reason: .methodNotAllowed)), nil)
            return
        }
        
        // Apply debug cookie
        
        if let debugCookie = debugCookie {
            HTTPCookieStorage.shared.setCookies(
                HTTPCookie.cookies(
                    withResponseHeaderFields: ["Set-Cookie": debugCookie],
                    for:url
            ), for: url, mainDocumentURL: url)
        }

        // Build our request

        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue

        if let headers = headers {
            for (key, value) in headers {
                request.setValue(value, forHTTPHeaderField: key)
            }
        }

        // If we are posting, safely retrieve the body and try to assign it to our request

        if !(body is NothingProtocol) {
            guard let body = body else {
                completion?(.failure(nil, NetworkError(reason: .buildingPayload)), nil)
                return
            }

            do {
                let result = try encode(body: body, type: endpoint.encodingType)
                request.httpBody = result.data
                request.setValue(result.headerValue, forHTTPHeaderField: "Content-Type")
            } catch {
                completion?(.failure(nil, NetworkError(reason: .buildingPayload)), nil)
                return
            }
        }
        
        // Build our response handler
        
        let task = session.dataTask(with: request as URLRequest) { (rawData, response, error) in

            // Print some logs to help track requests
            
            var debugOutput = "URL\n\(url)\n\n"
            
            if !(params is Nothing.Type) {
                debugOutput.append(contentsOf: "PARAMETERS\n\(params.asJSONString() ?? "No Parameters")\n\n")
            }
            
            if !(body is Nothing.Type) {
                debugOutput.append(contentsOf: "BODY\n\(body.asJSONString() ?? "No Body")\n\n")
            }
            
            if let responseData = rawData {
                debugOutput.append(contentsOf: "RESPONSE\n\(String(data: responseData, encoding: .utf8) ?? "No Response Content")")
            }
            
            Logging.client.record(debugOutput, domain: .network, level: .debug)

            guard let httpResponse = response as? HTTPURLResponse else {
                guard error == nil else {
                    completion?(.failure(nil, NetworkError(reason: .unwrappingResponse)), nil)
                    return
                }

                completion?(.failure(nil, NetworkError(reason: .invalidResponseType)), nil)
                return
            }

            let statusCode = httpResponse.statusCode

            // We have an error, return it

            guard error == nil, NetworkManager.successStatusRange.contains(statusCode) else {
                var output: Any?

                if let data = rawData {
                    output = (try? JSONSerialization.jsonObject(with: data,
                                                                options: .allowFragments)) ?? "Unable to connect"
                    
                    Logging.client.record("Response: \(String(data: data, encoding: .utf8) ?? "No error data")", domain: .network)
                }

                completion?(.failure(statusCode, NetworkError(reason: .requestFailed, json: output)), nil)
                return
            }

            // Safely cast the responseType we are expecting

            guard let responseType = responseType else {
                completion?(.failure(statusCode, NetworkError(reason: .castingToExpectedType)), nil)
                return
            }

            // If we are expecting nothing, return now (since we will have nothing!)

            if responseType is Nothing.Type {
                completion?(.success(statusCode), nil)
                return
            }

            guard let data = rawData else {
                assertionFailure("Could not cast data from payload when we passed pre-cast checks")
                return
            }

            // Decode the JSON and cast to our expected response type

            do {
                let decoder = JSONDecoder()
                decoder.dateDecodingStrategy = .iso8601
                let responseObject = try decoder.decode(responseType, from: data)
                completion?(.success(statusCode), responseObject)
                return
            } catch let error {
                let content = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
                Logging.client.record("Failed to build codable from JSON: \(String(describing: content))\n\nError: \(error)", domain: .network, level: .error)
                assertionFailure("Failed to build codable from JSON: \(error)")
                completion?(.failure(statusCode, NetworkError(reason: .castingToExpectedType)), nil)
                return
            }
        }

        // Submit our request

        task.resume()
    }
0

There are 0 answers