Issues parsing JSON data with Swift from API - swift

I am attempting to get data from an API and bring it into swift. I have managed to connect to th API and get the data come into the console however when I attempt to parse the data I have an issue - below is the function that does the connection and should parse the data.
I am having an issue with the line:
let fetchedData = try JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as! NSArray
It is producing the fatal error SIGABRT when running the app (the function parseData() is called within viewDidLoad()) and the console produces the following - Could not cast value of type '__NSDictionaryI' (0x103b7f288) to 'NSArray' (0x103b7ee28).
func parseData() {
let url = "https://api.***"
var request = URLRequest(url: URL(string: url)!)
let headers = [
"user": "***",
"auth": "***"
]
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request) { (data, response, error) in
if (error != nil) {
print("Error")
} else {
do {
let fetchedData = try JSONSerialization.jsonObject(with: data!, options: .mutableLeaves) as! NSArray
for eachFetchedOpportunity in fetchedData {
let eachOpportunity = eachFetchedOpportunity as! [String : Any]
let opportunity = eachOpportunity["subject"] as! String
let startDate = eachOpportunity["starts_at"] as! String
self.fetchedOpportunitys.append(Opportunity(opportunityName: opportunity, startDate: startDate))
}
print(self.fetchedOpportunitys)
} catch {
print("Error")
}
}
}
task.resume()
}
Here is the start of the returned data (I haven't included all of the data as some of it is private and unnecessary) from the server that I have accessed through postman - I believe my problem may reside in the fact that there is a mix of Dictionaries and Arrays here and I am unaware on how to correctly parse the data if this is the case.
{
"opportunities": [
{
"id": 3,
"store_id": 1,
"project_id": null,
"member_id": 5,
"billing_address_id": 5,
"venue_id": null,
"tax_class_id": 1,
"subject": "***",
"description": "",
"number": "00000000**",
"starts_at": "2017-05-27T08:00:00.000Z",
"ends_at": "2017-05-27T16:00:00.000Z",
"charge_starts_at": "2017-05-27T08:00:00.000Z",
"charge_ends_at": "2017-05-27T16:00:00.000Z",
"ordered_at": "2017-05-25T21:10:00.000Z",
"quote_invalid_at": null,
"state": 3,
...
...
}
],
"meta": {
"total_row_count": 2,
"row_count": 2,
"page": 1,
"per_page": 12
}
}

Your JSON response is Dictionary not Array and the array you are looking for is opportunities that you need to get from the dictionary.
do {
let fetchedData = try JSONSerialization.jsonObject(with: data!, options: []) as! [String:Any]
let opportunities = fetchedData["opportunities"] as? [[String:Any]] ?? []
for opportunity in opportunities {
let subject = opportunity["subject"] as? String ?? "Default Subject"
let startDate = opportunity["starts_at"] as? String ?? "Default Date"
self.fetchedOpportunitys.append(Opportunity(opportunityName: subject, startDate: startDate))
}
}
catch {
print("Error")
}

Related

Swift: getting nil when decoding API response

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()
}

Always finding nil when checking Double in Swift

I've got the following code:
//create an NSURL
let url = NSURL(string: self.urlString)
//fetch the data from the url
URLSession.shared.dataTask(with: (url as URL?)!, completionHandler: {(data, response, error) -> Void in
//If the retrieved information is a JSON Object, and can be treated as an NSArray...
if let jsonObj = (try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSObject) {
let data = jsonObj.value(forKey: "pendingloads") as! NSArray
for item in data {
if let itemObject = item as? NSObject {
print("Tons value1: \(itemObject.value(forKey: "tons")!)")
let tons = itemObject.value(forKey: "tons") as? Double ?? nil
print("Tons value2: \(String(describing: tons))")
I'm doing this because I have it's possible to receive null from this data. My issue is that I always receive a double value (when the item returns one) in the first print, but nil in the second. Is it because the value of itemObject.value(forKey: "tons") is Optional?
I've attempted to force unwrap the value, but it then breaks when it is null. I need it to be nullable, but I've had trouble doing it in every documented way. I know there's a very simple answer to this, but I just haven't found it yet. Any help would be very appreciated.
The relevant JSON:
}
"pendingloads": [
{
"comment": "Test Comment",
"hauler": "Test Hauler",
"logs": [
{
"coords": "(25.123456, -120.123456)",
"type": "auth",
"timestamp": "2019-04-04 10:52:1554393131",
"device_id": "DEVICE-ID-DEVICE-ID-DEVICEID"
},
{
"coords": "(25.123456, -120.123456)",
"type": "update",
"timestamp": "2019-04-08 13:38:1554748736",
"device_id": "DEVICE-ID-DEVICE-ID-DEVICEID"
}
],
"tons": "12.800",
"load_id": 23,
"requires_correction": false,
"trailer_drop": true,
"gross": "25600.000",
"contract_id": 3679,
"scaleticket": "2134098",
"destination": "TEST DESTINATION",
"sale_id": 3961,
"tare": "0.000",
"net": "25600.000",
"cull": "157.000",
"product": "Test Product",
"operator_id": 2674,
"hauler_id": 617,
"timestamp": "2019-04-08 18:38:1554766680",
"driver": "Terry",
"ticket": 250,
"product_id": 3172,
"sale": "Test Sale",
"trailer": "013724589"
}
]
}
In the hopes of improvement, here is the updated code:
//create a URL
let url = URL(string: self.urlString)
//fetch the data from the url
URLSession.shared.dataTask(with: (url)!, completionHandler: {(data, response, error) -> Void in
if let jsonObj = (try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as AnyObject) {
let data = jsonObj.value(forKey: "pendingloads") as! Array<AnyObject>
for item in data {
let itemObject = item as AnyObject
You can try
struct Root: Codable {
let pendingloads: [Pendingload]
}
struct Pendingload: Codable {
let comment, hauler: String
let logs: [Log]
let tons: String
let loadId: Int
let requiresCorrection, trailerDrop: Bool
let gross: String
let contractId: Int
let scaleticket, destination: String
let saleId: Int
let tare, net, cull, product: String
let operatorId, haulerId: Int
let timestamp, driver: String
let ticket, productId: Int
let sale, trailer: String
}
struct Log: Codable {
let coords, type, timestamp, deviceId: String
}
let url = URL(string: self.urlString)!
URLSession.shared.dataTask(with:url) { (data, response, error) in
guard let data = data else { return }
do {
let res = JSONDecoder()
res.keyDecodingStrategy = .convertFromSnakeCase
let ss = try res.decode(Root.self, from:data)
print(ss)
}
catch {
print(error)
}
}.resume()

How to make the right API call?

I am trying to access fixer.io by making an API call. It is the first time than I am trying to do so, but I don't get the result wanted. I would like to get the "rate" and the "result" from this JSON file.
{
"success": true,
"query": {
"from": "GBP",
"to": "JPY",
"amount": 25
},
"info": {
"timestamp": 1519328414,
"rate": 148.972231
},
"historical": ""
"date": "2018-02-22"
"result": 3724.305775
}
The method that I have implemented is this one, but I can not figure out how to retrieve "rate" and "result" when making this API call.
extension APIsRuler {
func getExchangeRate(from: String, to: String, amount: String, callback: #escaping (Bool, ConversionResult?) -> Void) {
var request = URLRequest(url: APIsRuler.exchangeURL)
let body = "convert?access_key=\(APIsRuler.exchangeAPI)&from=\(from)&to=\(to)&amount=\(amount)"
request.httpMethod = "GET"
request.httpBody = body.data(using: .utf8)
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: request) { (data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
return callback(false, nil)
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
return callback(false, nil)
}
guard let responseJSON = try? JSONDecoder().decode([String: Double].self, from: data),
let rate = responseJSON["rate"],
let result = responseJSON["result"] else {
return callback(false, nil)
}
let conversionResult = ConversionResult(exchangeRate: rate, exchangeResult: result)
callback(true, conversionResult)
}
}
task?.resume()
}
}
Use a real model object, like this:
struct Conversion: Codable {
let success: Bool
let query: Query
let info: Info
let historical, date: String
let result: Double
}
struct Info: Codable {
let timestamp: Int
let rate: Double
}
struct Query: Codable {
let from, to: String
let amount: Int
}
and parse your response into it using JSONDecoder:
do {
let conversion = try JSONDecoder().decode(Conversion.self, from: data)
let rate = conversion.info.rate
let result = conversion.result
} catch { print(error) }
You are mixing up two different APIs.
Either use JSONSerialization, the result is a dictionary and you get the values by key and index subscription. And you have to downcast every type and consider the nested rate value.
guard let responseJSON = try? JSONSerialization.jsonObject(with: data) as? [String:Any],
let info = responseJSON["info"] as? [String:Any],
let rate = info["rate"] as? Double,
let result = responseJSON["result"] as? Double else {
return callback(false, nil)
}
let conversionResult = ConversionResult(exchangeRate: rate, exchangeResult: result)
callback(true, conversionResult)
Or use JSONDecoder then you have to create structs, decoding to [String:Double] can work only if all values in the root object are Double which is clearly not the case.
struct Root: Decodable {
let info: Info
let result: Double
}
struct Info: Decodable {
let rate: Double
}
guard let responseJSON = try? JSONDecoder().decode(Root.self, from: data) else {
return callback(false, nil)
}
let conversionResult = ConversionResult(exchangeRate: responseJSON.info.rate, exchangeResult: responseJSON.result)
callback(true, conversionResult)
The code is only an example to keep your syntax. Practically you are strongly discouraged from using try? when decoding JSON. Always catch and handle errors
do {
let responseJSON = try JSONDecoder().decode(Root.self, from: data)
let conversionResult = ConversionResult(exchangeRate: responseJSON.info.rate, exchangeResult: responseJSON.result)
callback(true, conversionResult)
} catch {
print(error)
return callback(false, nil)
}

RESTful API Calls to Snowboy API with Swift 3

I'm a mobile app noobie currently trying to send 3 wav voice samples with Swift3 to the Snowboy API server.
Per their documentation Link Here, the request needs to have the following elements:
Endpoint: https://snowboy.kitt.ai/api/v1/train/
Type: POST
Content-Type: application/json
Required Parameter - token: Secret user token
Required Parameter - name: name of the recorded hotword that's mentioned in the voice samples
Required Parameter - voice_samples: A list of 3 voice samples in .wav format encoded as base64 strings
An example of the json they're expecting would look like this:
data = {
"name": "nameOfSample",
"language": "en",
"token": "token",
"voice_samples": [
{"wave": voicesample1asBase64String},
{"wave": voicesample2asBase64String},
{"wave": voicesample3asBase64String}
]
}
With the following code, I get a 400 status code. Meaning that it recognized the token parameter and authenticated my request but the latter is somehow malformatted:
lazy var session: URLSession = URLSession(configuration: self.conf)
let url: URL
init(url: URL){
self.url = url
}
func sendDataToURL (completion: #escaping JSONDictionaryHandler)
{
var request = URLRequest(url: self.url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let path1 = Bundle.main.path(forResource: "voicesample1", ofType: "wav")!
let path2 = Bundle.main.path(forResource: "voicesample2", ofType: "wav")!
let path3 = Bundle.main.path(forResource: "voicesample3", ofType: "wav")!
let paths = [path1, path2, path3]
let audioFileStrings = paths.map { (path: String) -> [String:String] in
let audioURL = URL(fileURLWithPath: path)
let filename = audioURL.lastPathComponent
if let base64String = try? Data(contentsOf: audioURL).base64EncodedString(){
//print(base64String)
return ["wave":base64String]
}else{return ["":""]}
}
let parameters = ["token": "XXXXXXXXXXXXXXX",
"name": "nameOfSample",
"language": "en",
"voice_samples": audioFileStrings
] as [String : Any]
guard let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) else {return}
print("sending\(parameters)")
request.httpBody = httpBody
let uploadTask = session.dataTask(with: request) { (data, response, error) in
if error == nil {
if let httpResponse = response as? HTTPURLResponse{
print(httpResponse)
switch httpResponse.statusCode{
case 200: //successful
if let receiveddata = data{
print("YAAAY! DATA! \(receiveddata)")
do{
let json = try JSONSerialization.jsonObject(with: receiveddata, options: [])
print(json)
}
catch{
print(error)
}
}
default:
print("Bad HTTP response code: \(httpResponse.statusCode)")
}
}
if let receivedData = data{
}
}
else {
print("Error \(error?.localizedDescription)")
}
}
uploadTask.resume()
}
I think it's the voice_samples list that isn't well inserted in the son. Does anybody know how I can construct the request so the Snowboy server accepts it? Thanks!
You need a complete list of parameters. Add these:
age_group
gender
microphone
This these parameters i got 201 error. But i used bad wav files.
let parameters = ["token": "XXXXXXXX",
"name": "nameOfSample123123123",
"language": "en",
"voice_samples": audioFileStrings,
"age_group": "0_9",
"gender": "M",
"microphone": "test"

Cannot invoke 'jsonObject' with an argument list of type

When using this code to get JSON from a server with Basic auth:
let config = URLSessionConfiguration.default
let userPasswordData = "\(username!):\(password!)".data(using: .utf8)
let base64EncodedCredential = userPasswordData!.base64EncodedString(options: Data.Base64EncodingOptions.init(rawValue: 0))
let authString = "Basic \(base64EncodedCredential)"
config.httpAdditionalHeaders = ["Authorization" : authString]
let session = URLSession(configuration: config)
let url = URL(string: "https://theforest.academy/api/v1/auth")
let task = session.dataTask(with: url!) {
(data, response, error) in
if (response as? HTTPURLResponse) != nil {
do {
if let data = data {
// let json1 = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let json = try JSONSerialization.jsonObject(with: data, options: [], error: []) as? [String: Any]
if(json["valid"]) {
print(json["token"])
} else {
print("Invalid login")
}
}
} catch {
print("Error deserializing JSON: \(error)")
}
I am getting the following error
Cannot invoke 'jsonObject' with an argument list of type '(with:
Data, options: [Any], error: [Any])'
Looking at the documentation, it seems that you only have two choices:
jsonObject(with: Data, options: JSONSerialization.ReadingOptions = [])
And
jsonObject(with: InputStream, options: JSONSerialization.ReadingOptions = [])
I think you are looking for the first method. Perhaps you are confusing it with
writeJSONObject(Any, to: OutputStream, options: JSONSerialization.WritingOptions = [], error: NSErrorPointer)
So, in short, such a method does not exist. That is why you are getting the error.
This is because we are not handling error while getting Data. So we need to safely type caste data and then pass it to JsonSerializer. Here is the code.
do {
let data = try NSData.init(contentsOfFile: path) as Data
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
print(jsonArray)
} catch {
print("Error getting data")
}