0
votes

When I hit my configuration API with Postman I am given the following json response back. In this response the two apiVersion keys are numbers and not strings.

{
    "data": {
        "availability": {
            "auth": true,
            "ab": true,
            "cd": true
        },
        "helloWorldConfiguration": {
            "apiKey": "abcefg",
            "rootUrl": "https://foo",
            "apiVersion": 3
        },
        "fooBarConfiguration": {
            "baseUrl": "https://foo",
            "apiVersion": 1,
            "privateApiPath": "",
            "publicApiPath": "dev",
            "tokenPath": ""
        }
    },
    "errors": []
}

When I try to decode it it fails with a typeMismatch error. When I output the contents of the response, I see the following which looks fine to me.

data = {
    availability = {
        auth = 1;
        ab = 1;
        cd = 1;
    };
    helloWorldConfiguration = {
        apiVersion = 1;
        baseUrl = "https://foo";
        privateApiPath = "";
        publicApiPath = dev;
        tokenPath = "";
    };
    fooBarConfiguration = {
        apiKey = abcefg;
        apiVersion = 3;
        rootUrl = "https://foo";
    };
};
errors =     (
);

The error given to me indicates that data.helloWorldConfiguration.apiVersion is of type string instead of int. We can see from the original HTTP response I get from Postman that's not the case.

typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), CodingKeys(stringValue: "helloWorldConfiguration", intValue: nil), CodingKeys(stringValue: "apiVersion", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil)) 21:17:40 ERROR Unable to decode the response data into a model representation.

My model represents those properties as integers so it would appear that it receives the response and considers those numbers to be strings, which they're not.

public struct ServerConfiguration: Decodable {
    let availability: AvailabilityConfiguration
    let helloWorldConfiguration: HelloWorldConfiguration
    let fooBarConfiguration: FooBarConfiguration

    init(availability: AvailabilityConfiguration, helloWorldConfiguration: HelloWorldConfiguration, fooBarConfiguration: FloatSinkConfiguration) {
        self.availability = availability
        self.helloWorldConfiguration = helloWorldConfiguration
        self.fooBarConfiguration = fooBarConfiguration
    }
}

public struct FooBarConfiguration: Decodable {
    let baseUrl: String
    let apiVersion: Int
    let privateApiPath: String
    let publicApiPath: String
    let tokenPath: String

    init(baseUrl: String, apiVersion: Int, privateApiPath: String, publicApiPath: String, tokenPath: String) {
        self.baseUrl = baseUrl
        self.apiVersion = apiVersion
        self.privateApiPath = privateApiPath
        self.publicApiPath = publicApiPath
        self.tokenPath = tokenPath
    }
}

public struct AvailabilityConfiguration: Decodable {
    let auth: Bool
    let ab: Bool
    let cd: Bool

    init(auth: Bool, ab: Bool, cd: Bool) {
        self.auth = auth
        self.ab = ab
        self.cd = cd
    }
}

public struct HelloWorldConfiguration: Codable {
    let apiKey: String
    let rootUrl: String
    let apiVersion: Int

    init(apiKey: String, rootUrl: String, apiVersion: Int) {
        self.apiKey = apiKey
        self.rootUrl = rootUrl
        self.apiVersion = apiVersion
    }
}

As you can see my apiVersion members are both of type integer along with the json response. What am I doing wrong here? I assume what's happening is Swift is considering the numbers in the json string, regardless of how they're actually represented in the json. Is that the case?

Edit to show utf8 string of Alamofire response data

21:44:06 INFO GET: https:foo/configuration
{
    "data" : {
        "availability" : {
            "auth" : true,
            "ab" : true,
            "cb" : true
        },
        "helloWorldConfiguration" : {
            "apiKey" : "abcd",
            "rootUrl" : "https://foo",
            "apiVersion" : "3"
        },
        "fooBarConfiguration" : {
            "baseUrl" : "https://foo",
            "apiVersion" : "1",
            "privateApiPath" : "",
            "publicApiPath" : "dev",
            "tokenPath" : "auth/token"
        }
    },
    "errors" : []
}

It would seem that despite the API correctly returning apiVersion as a number, Swift is turning it into a string. Am I decoding it incorrectly?

func getRoute<TResponseData: Decodable>(route:String, completion: @escaping (TResponseData) -> Void) throws {
    let headers = try! self.getHeaders(contentType: ContentType.json)
    let completeUrl: String = self.getUrl(route: route, requestUrl: nil)
    logger.info("GET: \(completeUrl)")
Alamofire.request(
    completeUrl,
    method: .get,
    parameters: nil,
    encoding: JSONEncoding.default,
    headers: headers)
    .validate()
    .responseJSON { (response) -> Void in
        self.logger.info("GET Response: \(String(describing:response.response?.statusCode))")

        switch response.result {
        case .success(_):
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .custom(Date.toFooBarDate)
            do {
                let result = try decoder.decode(TResponseData.self, from: response.data!)
                completion(result)
            } catch DecodingError.dataCorrupted(let error) {
                self.logger.error(error.underlyingError!)
                return
            } catch {
                print(response.result.value!)
                print(error)
                self.logger.error("Unable to decode the response data into a model representation.")
                return
            }
        }
    }
1
Please create a String from the output and print this. The collection type output is ambiguous, because you cannot distinguish String and Int. And you can delete all initializers in the structs. You get them for free.vadian
String representation of the Alamofire data provided. It does indeed appear to be a string, despite the API returning it as a number. Why would that be the case and how can I fix that?Johnathon Sullinger
apiversion is not and Int. Its string. Change it to String in your structAmit
In the Alamofire response output apiVersion is clearly String. Everything in double quotes is String. So the decoder error message is correct (actually it's always correct 😉 ).vadian
No, simply changing a data type should not be the solution. What if I actually need that to be a number so I ca do math or store it in a local DB as an integer? Hacking together a conversion after the fact is sloppy and a bandaid to the underlying issue of Alamofire’s data property having numbers decoded as strings. As mentioned earlier their response.result.value prints it correct though. Perhaps I just convert it to json string before decoding it.Johnathon Sullinger

1 Answers

0
votes

I checked in my playground and it seems that everything is working fine.To find the real issue i think you are required to provide the real url from where you are getting json and can be checked with alamofire

import Foundation
let json = """
{
"data": {
    "availability": {
        "auth": true,
        "ab": true,
        "cd": true
    },
    "helloWorldConfiguration": {
        "apiKey": "abcefg",
        "rootUrl": "https://foo",
        "apiVersion": 3
    },
    "fooBarConfiguration": {
        "baseUrl": "https://foo",
        "apiVersion": 1,
        "privateApiPath": "",
        "publicApiPath": "dev",
        "tokenPath": ""
    }
},
"errors": []
 }
 """
let data = json.data(using: .utf8)


struct Response : Codable {
let data : Data?
let errors : [String]?
 }
 struct Availability : Codable {
let auth : Bool?
let ab : Bool?
let cd : Bool?
 }

 struct Data : Codable {
let availability : Availability?
let helloWorldConfiguration : HelloWorldConfiguration?
let fooBarConfiguration : FooBarConfiguration?
 }

struct FooBarConfiguration : Codable {
let baseUrl : String?
let apiVersion : Int?
let privateApiPath : String?
let publicApiPath : String?
let tokenPath : String?
}
struct HelloWorldConfiguration : Codable {
let apiKey : String?
let rootUrl : String?
let apiVersion : Int?
}


 let decoder = JSONDecoder()

 let response = try decoder.decode(Response.self, from: data!)
 print(response)

And here is the response

Response(data: Optional(__lldb_expr_11.Data(availability: Optional(__lldb_expr_11.Availability(auth: Optional(true), ab: Optional(true), cd: Optional(true))), helloWorldConfiguration: Optional(__lldb_expr_11.HelloWorldConfiguration(apiKey: Optional("abcefg"), rootUrl: Optional("https://foo"), apiVersion: Optional(3))), fooBarConfiguration: Optional(__lldb_expr_11.FooBarConfiguration(baseUrl: Optional("https://foo"), apiVersion: Optional(1), privateApiPath: Optional(""), publicApiPath: Optional("dev"), tokenPath: Optional(""))))), errors: Optional([]))