Issue with JSON decoding in Swift (data missing) - json

I'm trying to query the NASA image API (latest docs here) using Swift 4. I set up and tested my request with JSONPlaceholder to make sure my network request and decoding was setup correctly. Everything was working fine, but when I switched the URL and corresponding JSON data structure, I get an error saying 'the data couldn't be read because it is missing.'
I've used Postman to verify that JSON is being returned, and to build the JSON data structure.
Is this a common error from decoding JSON or is it something with the network request? Or am I missing something with using the NASA API?
let NASAURL = URL(string: "https://images-api.nasa.gov/search?q=moon")
let session = URLSession(configuration: .default)
let task = session.dataTask(with: NASAURL!) { (rdata, response, error) in
NSLog("Data Description: " + (rdata!.debugDescription) + "\nResponse: " + response.debugDescription + "\nError Description: " + error.debugDescription)
guard rdata != nil else{
NSLog("No data")
return
}
guard error == nil else{
NSLog(response.debugDescription + "\n")
NSLog(error.debugDescription)
NSLog(error.debugDescription)
return
}
let decoder = JSONDecoder()
do{
NSLog(rdata.debugDescription)
let usr = try decoder.decode(Collect.self, from: rdata!) // Throws
NSLog(usr.href)
} catch {
NSLog("Error: " + error.localizedDescription)
}
}
task.resume()
// Collect is in its own class/file
struct Collect: Codable {
var href: String
//var items: [Items]
}
Below is the printout from the above log statements...
2017-09-29 19:50:24.135288-0500 OpenNASA[16993:10774203] Data Description: 67669 bytes
Response: Optional(<NSHTTPURLResponse: 0x60000003db00> { URL: https://images-api.nasa.gov/search?q=moon } { status code: 200, headers {
"Access-Control-Allow-Origin" = "*";
"Cache-Control" = "public, max-age=300, s-maxage=600";
"Content-Encoding" = gzip;
"Content-Length" = 9334;
"Content-Type" = "application/json; charset=UTF-8";
Date = "Sat, 30 Sep 2017 00:48:11 GMT";
Server = "nginx/1.4.6 (Ubuntu)";
"Strict-Transport-Security" = "max-age=31536000";
Vary = "Accept-Encoding";
"access-control-allow-headers" = "Origin,Content-Type,Accept,Authorization,X-Requested-With";
"access-control-allow-methods" = GET;
} })
Error Description: nil
2017-09-29 19:50:24.137324-0500 OpenNASA[16993:10774203] Optional(67669 bytes)
2017-09-29 19:56:01.843750-0500 OpenNASA[16993:10774203] Error: The data couldn’t be read because it is missing.

You Codable should be like below:
struct Collect: Codable {
var collection: Links
}
struct Links: Codable {
var links: [Href]
}
struct Href: Codable {
var href: String
}
You have to call like below:
let usr = try decoder.decode(Collect.self, from: rdata!) // Throws
let links = usr.collection.links
for link in links {
print(link.href)
}

Related

I have nested data in a JSON file and I am using a nested struct. How can I access the values that are nested within the first struct in swift

Here is my code. I am pulling JSON data from CalorieNinjas API:
struct Result: Codable {
var items: [FoodItem]?
}
struct FoodItem: Codable {
var name: String?
var calories: String?
}
public class API {
func apiRequest(search: String, completion: #escaping (Result) -> ()) {
//URL
var query = search.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let url = URL(string: "https://calorieninjas.p.rapidapi.com/v1/nutrition?query=" + query!)
//URL REQUEST
var request = URLRequest(url: url!, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
//Specify header
let headers = [
"x-rapidapi-key": "3be44a36b7msh4d4738910c1ca4dp1c2825jsn96bcc44c2b19",
"x-rapidapi-host": "calorieninjas.p.rapidapi.com"
]
request.httpMethod="GET"
request.allHTTPHeaderFields = headers
//Get the URLSession
let session = URLSession.shared
//Create data task
let dataTask = session.dataTask(with: request) { (data, response, error) in
let result = try? JSONDecoder().decode(Result.self, from: data!)
print(result)
DispatchQueue.main.async {
completion(result!)
}
}
//Fire off data task
dataTask.resume()
}
}
this is what my view looks like:
struct ContentView: View {
#State var result = Result()
#State private var searchItem: String = ""
var body: some View {
ZStack(alignment: .top) {
Rectangle()
.fill(Color.myPurple)
.ignoresSafeArea(.all)
VStack {
TextField("Enter food", text: $searchItem)
.background(Color.white)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
SearchButton()
.padding(.top)
.onTapGesture {
API().apiRequest(search: searchItem, completion: { (result) in
self.result = result
})
}
}
}
}
}
This is the output to the terminal as a result of my print statement so I know my data is being fetched and stored:
Optional(CalorieCountApp.Result(items: Optional([CalorieCountApp.FoodItem(name: Optional("pizza"), calories: Optional(262.9))])))
what I was trying to do was something like Text(result.items.name/calories) but I am not able to access the variables like that. I am new to swift and making apps as a whole any help is much appreciated
Looks like you have a few Optionals in there, which means you'll probably be using the ? operator to unwrap them.
Given your type, this should work:
let index = 0
let name = result?.items?[index].name // will be `String?`
let calories = result?.items?[index].calories // according to your code you provided, this says `String?` but in your console output it looks like `Double?`
or in your example:
Text(result?.items?[index].name ?? "unknown")
You might want to do some more reading about unwrapping Optionals or dealing with nil in Swift -- there are a few different strategies. For example, you can see I used ?? in the last example there.
Here's a helpful link: https://www.hackingwithswift.com/sixty/10/2/unwrapping-optionals

How to get data from JSON response in iOS Swift?

Here i have using swiftyJson pod library for response data. normal json response data i could able to get data but for complex i could not make it.
here is my code to get data from response:
private func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse) {
let user = "David****"
let password = "**************"
let loginString = "\(user):\(password)"
guard let loginData = loginString.data(using: String.Encoding.utf8) else {
return
}
let base64LoginString = loginData.base64EncodedString()
print("base 64 login :\(base64LoginString)")
let headers = ["Authorization": "Basic \(base64LoginString)"]
// using URL and request getting a json
let request = URLRequest(url: NSURL(string: path)! as URL)
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = headers
let session = URLSession.init(configuration: config)
session.dataTask(with: request) { (data:Data?, response: URLResponse?, error:Error?) in
if let jsonData = data { // if data has a data and success
do {
let json: JSON = try JSON(data: jsonData)
onCompletion(json,nil)
print("json data:\(json)")
}catch {// error
onCompletion(JSON(),error)
}
} else { // if the data is nil
onCompletion(JSON(),error)
}
}.resume()
}
Used this function in viewController.swift
func addDummyData() {
// Call API
RestApiManager.sharedInstance.getRandomUser { (json:JSON) in
// return json from API
if let results = json["results"].array { // get results data from json
print("results:\(results)")
for entry in results { // save data to items.
self.items.append(UserObject(json: entry))
}
print("array= \(self.items)")
DispatchQueue.main.async { // back to the main que and reload table
self.tableView.reloadData()
}
}
}
}
Model class:
import SwiftyJSON
class UserObject {
// var pictureURL: String!
var username: String!
required init(json: JSON) {
// pictureURL = json["picture"]["medium"].stringValue
username = json["WorkOrder"].stringValue
}
}
Here is my json response:
{
"d": {
"results": [
{
"__metadata": {
"id": "http://*******:****/sap/opu/odata/sap/ZPRJ_PM_APPS_IH_SRV/WorkOrderF4Set('000000504780')",
"type": "ZPRJ_PM_APPS_IH_SRV.WorkOrderF4"
},
"WorkOrder": "000000504780",
"Description": "General Maintenance testing"
},
}
}
From json response i'm trying to get WorkOrder and Description
Any help much appreciated pls....
Please read the JSON carefully. The outermost object is a dictionary with a key d.
To get the results array you have to write
if let results = json["d"]["results"].array { ...
And you don't need a class and never declare properties as IUO which are initialized in an init method
struct User {
let workOrder: String
let description: String
init(json: JSON) {
workOrder = json["WorkOrder"].stringValue
description = json["Description"].stringValue
}
}
Side note: Since Swift 4 SwiftyJSON has become obsolete in favor of Codable. It's built-in and much more efficient.

Swift 3 GET request unable to display returned values

Trying to make a GET request with Swift 3 and Xcode8 but not having much luck outputting data I received back from my GET request to the Main.Storyboard either in a label or a text field all I get is all the returned JSON in the console in Xcode8.
There is a snippet of JSON that is returned with my request in Xcode8 which I have attached below for reference. I can access "#encoding" and "#version" but everything else is not able to be accessed and I am not able to figure out why. If this was a web based setup I could just declare a variable and then set it equal to someObject.petfinder[1].shelters etc...
This is my code that is making the request, taking a zipcode from an input field and then building the url and returning it which then is using NSDictionary.
The error I get back in the console is:
"fatal error: unexpectedly found nil while unwrapping an Optional value
2017-06-20 12:15:49.392688 PetFinder[47445:9486946] fatal error: unexpectedly found nil while unwrapping an Optional value"
// SEND HTTP GET REQUEST
// DEFINE SERVER SIDE SCRIPT URL
let scriptUrl = "https://api.petfinder.com/"
let methodType = "shelter.find"
let apiKey = "?key=0000000000000000000000000"
let urlWithParams = scriptUrl + methodType + apiKey + "&location=\(shelterZip)&format=json"
// CREATE NSURL Object
let myUrl = NSURL(string: urlWithParams)
// CREATE URL REQUEST
let request = NSMutableURLRequest(url:myUrl! as URL);
// REQUEST METHOD - GET / POST
request.httpMethod = "GET"
// RUN HTTP REQUEST
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print("error \(error!)")
return
}
// PRINT OUT RESPONSE STRING
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("responseString = \(String(describing: responseString))")
// CONVERT RECEIVED JSON TO NSDictionary
do {
if let convertedJsonIntoDict = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary {
// Print out dictionary
print(convertedJsonIntoDict)
// Get value by key
let shelterName = convertedJsonIntoDict["shelters"] as? [String: Any]
for(key, pair) in convertedJsonIntoDict {
print("-->\(key) \(pair)")
}
print(shelterName!)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
task.resume()
}
// SNIPPET OF RETURNED JSON FROM THE TOP
{
"#encoding" = "iso-8859-1";
"#version" = "1.0";
petfinder = {
"#xmlns:xsi" = "http://www.w3.org/2001/XMLSchema-instance";
"#xsi:noNamespaceSchemaLocation" = "http://api.petfinder.com/schemas/0.9/petfinder.xsd";
header = {
status = {
code = {
"$t" = 100;
};
message = {
};
};
timestamp = {
"$t" = "2017-06-20T16:15:49Z";
};
version = {
"$t" = "0.1";
};
};
lastOffset = {
"$t" = 25;
};
shelters = {
shelter = (
{
address1 = {
};
address2 = {
};
city = {
"$t" = Nebraska;
};
country = {
"$t" = US;
};
email = {
"$t" = "wooffun#woof.net";
};
fax = {
};
id = {
"$t" = NE117;
};
latitude = {
"$t" = "13.004";
};
longitude = {
"$t" = "-31.449";
};
name = {
"$t" = WOOF COMPANY;
};
phone = {
};
state = {
"$t" = NE;
};
zip = {
"$t" = 68001;
};
},
First of all – and as always
Do not use NSDictionary in Swift. You throw away the type information.
Do not use Foundation classes like NSURL, NS(Mutable)URLRequest if there is a native Swift counterpart.
A GET request does not require an URLRequest at all, it's the default.
Second of all lets use a type alias for convenience reasons.
typealias JSONDictionary = [String:Any]
This code is supposed to extract the keys and values you are looking for
let scriptUrl = "https://api.petfinder.com/"
let methodType = "shelter.find"
let apiKey = "?key=0000000000000000000000000"
let urlWithParams = scriptUrl + methodType + apiKey + "&location=\(shelterZip)&format=json"
// CREATE NSURL Object
let myUrl = URL(string: urlWithParams)!
// RUN HTTP REQUEST
let task = URLSession.shared.dataTask(with: myUrl) { data, response, error in
if error != nil {
print("error \(error!)")
return
}
// CONVERT RECEIVED JSON TO NSDictionary
do {
if let rootDictionary = try JSONSerialization.jsonObject(with: data!) as? JSONDictionary,
let petfinder = rootDictionary["petfinder"] as? JSONDictionary {
// Get value by key
if let shelters = petfinder["shelters"] as? JSONDictionary,
let shelter = shelters["shelter"] as? [JSONDictionary] {
for item in shelter {
for (key, value) in item {
print("-->\(key) \(value)")
}
}
}
}
} catch {
print(error.localizedDescription)
}
}
task.resume()

Sending post data nsurlsession

Hi i'm trying to replicate the following curl command in swift using NSURLSession in a playground.
curl -k -i -H "Accept: application/json" -H "X-Application: " -X POST -d 'username=&password=' https://address.com/api/login
Here's what I've got so far. Where i'm struggling is that i'm unsure how to send the post data e.g.: 'username=&password='. Any help would be appreciated.
Thanks in advance.
import Foundation
import XCPlayground
// Let asynchronous code run
XCPSetExecutionShouldContinueIndefinitely()
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
config.HTTPAdditionalHeaders = ["Accept" : "application/json", "X-Application" : "<AppKey>"]
let session = NSURLSession(configuration: config)
var running = false
let url = NSURL(string: "https://address.com/api/login")
let task = session.dataTaskWithURL(url!) {
(let data, let response, let error) in
if let httpResponse = response as? NSHTTPURLResponse {
let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
println(dataString)
}
running = false
}
running = true
task.resume()
You can create a mutable URLRequest and set the httpBody. But you should also percent escape the values for username and, more importantly, for password.
So, imagine your request being created like so:
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = ["Accept" : "application/json", "X-Application" : "<AppKey>", "Content-Type" : "application/x-www-form-urlencoded"]
let session = URLSession(configuration: config)
let url = URL(string: "https://identitysso.betfair.com/api/login")!
var request = URLRequest(url: url)
request.setBodyContent(["username": username, "password": password])
let task = session.dataTask(with: request) { data, response, error in
// make sure there wasn't a fundamental networking error
guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
print(error ?? "Unknown error")
return
}
// if you're going to check for NSHTTPURLResponse, then do something useful
// with it, e.g. see if server status code indicates that everything is OK
guard 200 ..< 300 ~= response.statusCode else {
print("statusCode not 2xx; was \(response.statusCode)")
return
}
// since you set `Accept` to JSON, I'd assume you'd want to parse it;
// In Swift 4 and later, use JSONDecoder; in Swift 3 use JSONSerialization
do {
if let responseObject = try JSONSerialization.jsonObject(with: data) as? [String: AnyObject] {
print(responseObject)
}
} catch let parseError {
print(parseError)
print(String(data: data, encoding: .utf8) ?? data as NSData)
}
}
task.resume()
So the question is how setBodyContent builds the request body given a dictionary. Yes, you want to percent-escape anything not in the unreserved character set, but sadly CharacterSet.urlQueryAllowed is not up to the job. So you might do something like:
extension URLRequest {
/// Populate the HTTPBody of `application/x-www-form-urlencoded` request
///
/// - parameter parameters: A dictionary of keys and values to be added to the request
mutating func setBodyContent(_ parameters: [String : String]) {
let parameterArray = parameters.map { (key, value) -> String in
let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
return "\(encodedKey)=\(encodedValue)"
}
httpBody = parameterArray
.joined(separator: "&")
.data(using: .utf8)
}
}
extension CharacterSet {
/// Character set containing characters allowed in query value as outlined in RFC 3986.
///
/// RFC 3986 states that the following characters are "reserved" characters.
///
/// - General Delimiters: ":", "#", "[", "]", "#", "?", "/"
/// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
///
/// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow
/// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/"
/// should be percent-escaped in the query string.
///
/// - parameter string: The string to be percent-escaped.
///
/// - returns: The percent-escaped string.
static let urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]#" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
return allowed
}()
}
Furthermore, I generally use a more complicated setBodyContent that also accepts numeric, boolean, and date types, but I didn't want to digress too far from your core question, how to properly build request for two string key/values pairs.
For Swift 2 rendition, see previous revision of this answer.
maybe this help, i use it to send post data:
var paramString = ""
var request:NSMutableURLRequest = NSMutableURLRequest(URL:url)
request.HTTPMethod = "POST"
var user = "MyUsername".stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
var pass = "MyPassword".stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
paramString = "username="+user!+"&password="+pass!
request.HTTPBody = paramString.dataUsingEncoding(NSUTF8StringEncoding)
let conf: NSURLSessionConfiguration = NSURLSession.sharedSession().configuration
let session = NSURLSession(configuration: conf)
session.dataTaskWithRequest(request) {
data, response, error in
let resp = NSString(data: data, encoding: NSUTF8StringEncoding)
}.resume()
NSUTF8StringEncoding can be replaced with whatever you need

Trouble parsing JSON with Swift using SwiftyJSON

I am new to swift and I am trying to parse some simple JSON data I am retrieving from a private API that I have. I am using the SwiftJSON library.
No matter what I do I cannot assign the variable "videoUploadId" with the value of "video_upload_id" coming from the JSON response. I hope i provided enough info to get some help. Thanks
Here is a segment of the code
let task = session.dataTaskWithRequest(request, completionHandler: { (data : NSData!, response, error : NSError!) -> Void in
if (error == nil) {
// Success
let statusCode = (response as NSHTTPURLResponse).statusCode
println("Status Code: \(statusCode)\r\n")
println("Response: \(response)\r\n")
println("Data: \(data)\r\n")
let dataContent = NSString(data: data!, encoding: NSUTF8StringEncoding)!
println("UTF8 Data: \(dataContent)\r\n")
let json = JSON(dataContent)
if let videoUploadId = json["video_upload_id"].int {
println("Video Upload ID (Dict: int): \(videoUploadId)")
}
else if let videoUploadId = json["video_upload_id"].string {
println("Video Upload ID (Dict: string): \(videoUploadId)")
}
else if let videoUploadId = json[0].int {
println("Video Upload ID (Array: int): \(videoUploadId)")
}
else if let videoUploadId = json[0].string {
println("Video Upload ID (Array: string): \(videoUploadId)")
}
else {
println(json["video_upload_id"].error)
}
}
else {
// Failure
println("URL Session Task Failed: %#", error.localizedDescription);
}
})
task.resume()
This is what I am receiving from my console:
Login Response: HTTP 200
Status Code: 201
Response: { URL: https:////videos/uploads/ } { status code: 201, headers {
Connection = "Keep-alive";
"Content-Length" = 24;
"Content-Type" = "application/json";
Date = "Sun, 25 Jan 2015 01:02:42 GMT";
Location = "https:////videos/uploads/";
Server = "Apache/2.2.15 (CentOS)";
"Set-Cookie" = "session=eyJzZXNzaW9uX3Rva2VuIjp7IiBiIjoiUzAxWGFYRnlVVGM1YjBsa1kxWkJiV2xrYVZwcFZXdDFiR0ZLYW5GQ1VqRjFjbk5GIn19.B6XSMg.HXatQ76ZFaoZEQsnNu1BgsVECKA; HttpOnly; Path=/";
} }
Data: <7b227669 64656f5f 75706c6f 61645f69 64223a20 3736307d>
UTF8 Data: {"video_upload_id": 760}
Optional(Error Domain=SwiftyJSONErrorDomain Code=901 "Dictionary["video_upload_id"] failure, It is not an dictionary" UserInfo=0x170238620 {NSLocalizedDescription=Dictionary["video_upload_id"] failure, It is not an dictionary})
As you can see from the code and console output, I am attempting to set the variable in several different ways all of which seem to fail. I am receiving the error "Dictionary["video_upload_id"] failure, It is not an dictionary" I even tried prepending "[" and appending "]" to try to see if its a formatting issue.
Any clues?
You are doing the initialization wrong. You should use:
let json = JSON(data:data) // data is NSData!
Converting NSData to NSString is not necessary, and somehow wrong for this. SwiftyJSON can only be initialized with NSData or Swift object.