Accessing JSON Array from API response in Swift - json

I am brand new to writing swift so any tips on improvement/best practices are welcome but my main issue is that I am having trouble accessing a nested JSON array list. I am using this free API and trying to show a list of characters https://swapi.dev/api/people/
Please see the code snippet below.
When I print the type its : Optional<Any> and when i print json["results"] it prints the array like:
Optional(<__NSArrayI 0x600000fe31e0>(
{
"birth_year" = 19BBY;
created = "2014-12-09T13:50:51.644000Z";
....
I have tried several different things but have been unsuccessful. Could someone please give some advice on how I might iterate the list under json["results"?
func onLoad() -> Void {
let url = URL(string: "https://swapi.dev/api/people")
guard let requestUrl = url else { fatalError() }
// Create URL Request
var request = URLRequest(url: requestUrl)
// Specify HTTP Method to use
request.httpMethod = "GET"
// Send HTTP Request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Check if Error took place
if let error = error {
print("Error took place \(error)")
return
}
// Convert HTTP Response Data to a simple String
if let data = data {
// let json = try? JSONSerialization.jsonObject(with: data, options: [])
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
// try to read out a string array
print(type(of: json["results"]))
print(json["results"])
}
} catch let error as Error {
print("Failed to load: \(error.localizedDescription)")
}
}
}
task.resume()
}
Thanks for any help!

You should really be using Decodable rather than trying to parse it with JSON as that can easily lead to errors as you are accessing values by strings and it doesn't allow the IDE to help you.
You need to create some objects that describe what you are getting in your response.
Your main json response is made up of the following
{
"count": 82,
"next": "http://swapi.dev/api/people/?page=2",
"previous": null,
"results": [...]
}
This allows you to create a People struct that conforms to Decodable.
struct People: Decodable {
let count: Int
let next: URL?
let previous: URL?
let results: [Person]
}
The results array is really what you are after as that contains all the information about a person.
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "http://swapi.dev/api/planets/1/",
"films": [
"http://swapi.dev/api/films/1/",
"http://swapi.dev/api/films/2/",
"http://swapi.dev/api/films/3/",
"http://swapi.dev/api/films/6/"
],
"species": [],
"vehicles": [
"http://swapi.dev/api/vehicles/14/",
"http://swapi.dev/api/vehicles/30/"
],
"starships": [
"http://swapi.dev/api/starships/12/",
"http://swapi.dev/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "http://swapi.dev/api/people/1/"
}
We can represent this with the following struct called Person that also conforms to Decodable
struct Person: Decodable {
let name: String
let height: String
let mass: String
let hairColor: String
let skinColor: String
let birthYear: String
let gender: Gender
let homeworld: String
let films: [URL]
let species: [URL]
let vehicles: [URL]
let starships: [URL]
let created: Date
let edited: Date
let url: URL
}
enum Gender: String, Decodable {
case male
case female
case unknown = "n/a"
}
Note a couple of differences between the names in the struct and the names in the object that you are getting back. eg hair_color (snakecase) and hairColor (camelCase) In Swift it is common to write it the latter way and when we use decodable we can tell our decoder to use a custom key decoding strategy. Also note that I have used an enum for Gender. This isn't required and we could have just used a String. Also note that created and edited are Dates, however they are not iso8601 compliant but we can also specify a custom date decoding strategy.
Here is how we can decode the data that you have received.
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let people = try decoder.decode(People.self, from: data)
Now we can put this all together in your network request to get the following:
func onLoad() {
let url = URL(string: "https://swapi.dev/api/people")
guard let requestUrl = url else { fatalError() }
// Create URL Request
var request = URLRequest(url: requestUrl)
// Specify HTTP Method to use
request.httpMethod = "GET"
// Send HTTP Request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Check if Error took place
if let error = error {
print("Error took place \(error)")
return
}
// Convert HTTP Response Data to a simple String
if let data = data {
do {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let people = try decoder.decode(People.self, from: data)
people.results.forEach { person in print(person) }
} catch {
print("Failed to load: \(error)")
}
}
}
task.resume()
}

Cast results as an Array of Dictionary. Here's how
if let data = data {
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let results = json["results"] as? [[String: Any]] {
for result in results {
print(result)
}
}
} catch {
print("Failed to load: \(error.localizedDescription)")
}
}
Better Approach: Use Codable, JSONSerialization feels bit outdated.
Related Links:
https://developer.apple.com/documentation/swift/codable
https://www.swiftbysundell.com/basics/codable/

Related

Nested Json data won't be decoded using Swift language?

I'm receiving response data from an API, when I'm trying to decode it by using json decoder, the nested json data won't be decoded because it returns null.
json data as follow:
{
"token": "string",
"details": {
"ID": "string",
"Name": "string",
"Message": null
}
}
Decoding model is:
struct response: Codable {
let token: String?
let usrData: userData?
}
struct userData:Codable{
let ID,Name,Message: String?
}
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
completion(.failure(.custom(errorMessage: "Please check internet connection")))
return
}
guard let loginResponse = try? JSONDecoder().decode(response.self, from:data) else
{
completion(.failure(.invalidCredentials))
return
}
print(loginResponse.userData?.userID as Any) //returns nil
print(loginResponse.token) //token printed
guard let token = loginResponse.token else {
completion(.failure(.invalidCredentials))
return
}
completion(.success(token))
}.resume()
The token from the response will be successfully decoded, but the userData returns null.
Your model's property name and decoded json property name must be equal if your are not mapping , so change your struct with :
struct response: Codable {
let token: String?
let details: userDto?
}
That certainly can not work, first, in your response struct you have a let variable that says usrData that can not be identified as details. Second you write usrData: userDto what is userDto you clearly did a mistake or forgot to mention it. However, do it like that for example:
struct Response: Codable {
let token: String?
let details: UserData?
}
struct UserData: Codable {
let ID,Name,Message: String?
}
let filePath = Bundle.main.path(forResource:"test", ofType: "json")
let data = try Data(contentsOf: URL(fileURLWithPath: filePath!))
if let loginResponse = try? JSONDecoder().decode(Response.self, from: data) {
loginResponse
}
The example is not completely correct, because fileURLWithPath is deprecated but you should get the idea from it.
I also recommend following some basic roles, like writing Structs with an uppercase letter.

How to decode JSON response from API in SwiftUI to use in a view

I'm pretty new to developing apps and working with Xcode and Swift and trying to get a handle on how to decode JSON responses from an API. I'm also trying to follow MVVM practices.
I'm calling to an API that returns data structured like this:
"location": {
"latitude": -27.4748,
"longitude": 153.017 },
"date": "2020-12-21",
"current_time": "21:55:42.198",
"sunrise": "04:49",
"sunset": "18:42",
"...": "..."
}
My understanding is I need a struct to decode this information into. This is what my struct looks like (it may be incorrect):
struct Example: Decodable {
let location: Coordinates
let date: String
let current_time: String
let sunset: String
let sunset: String
let ... :String
struct Coordinates: Decodable{
let latitude: Double
let longitude: Double
}
}
So I want to be able to call this api. I have the correct address to call it because the dashboard on the website I'm using is showing I've hit it. Could I get some guidance on how to call it and decode the response? Currently I'm doing something like this:
if let url = URL(string: "this is the api web address"){
URLSession.shared.dataTask(with: url){ (with: data, options [] ) as?
[String:String]{
print("(\json)" + "for debugging purposes")
}}
else{
print("error")
}
.resume()
Thanks for any and all help. Again, my goal is to call this function, hit the API, decode the JSON response and store it into a variable I can use in my views elsewhere. Thanks!
You are mixing JSONSerialization with Codable and URLSession. Forget about JSONSerialization. Try to focus on Codable protocol.
struct Example: Decodable {
let location: Coordinates
let date: String
let currentTime: String
let sunrise: String
let sunset: String
}
struct Coordinates: Decodable {
let latitude: Double
let longitude: Double
}
This is how you can decode your json data synchronously
extension JSONDecoder {
static let shared: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
}
extension Data {
func decodedObject<T: Decodable>() throws -> T {
try JSONDecoder.shared.decode(T.self, from: self)
}
}
Playground testing
let json = """
{
"location": {
"latitude": -27.4748,
"longitude": 153.017 },
"date": "2020-12-21",
"current_time": "21:55:42.198",
"sunrise": "04:49",
"sunset": "18:42",
}
"""
let data = Data(json.utf8)
do {
let example: Example = try data.decodedObject()
print(example)
} catch {
print(error)
}
And fetching your data asynchronously
extension URL {
func getResult<T: Decodable>(completion: #escaping (Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: self) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error!))
return
}
do {
completion(.success(try data.decodedObject()))
} catch {
completion(.failure(error))
}
}.resume()
}
}
let url = URL(string: "https://www.example.com/whatever")!
url.getResult { (result: Result<Example, Error>) in
switch result {
case let .success(example):
print(example)
case let .failure(error):
print(error)
}
}
If you need help with your SwiftUI project implementing MVVM feel free to open a new question.
Correct Method to decode a local JSON file in SwiftUI
do {
let path = Bundle.main.path(forResource: "filename", ofType: "json")
let url = URL(fileURLWithPath: path!)
let data = try Data(contentsOf: url)
let decodedresults = try JSONDecoder().decode(struct_name.self, from: data)
print(decodedresults)
} catch {
print(error)
}

fetching Json swift 5

im trying to fetch some data from my api in swift 5
I get this error
Member 'success' in 'Result<[Suggestion], SuggestionError>' produces result of type 'Result', but context expects 'Result<[Suggestion],
and this is the function that gives me problems. if I comment the line giving me trouples it prints canNotProcessData
func getSuggestions (completion: #escaping(Result<[Suggestion], SuggestionError>) -> Void){
let dataTask = URLSession.shared.dataTask(with: resourceURL) { data, response, error in
guard let jsonData = data else{
completion(.failure(.noDataAvailable))
return
}
print(response!)
do{
let decoder = JSONDecoder()
let suggestionReponse = try decoder.decode(Suggestion.self, from: jsonData)
let suggestions = suggestionReponse
completion(.success(Suggestion))
}catch{
completion(.failure(.canNotProcessData))
}
}
dataTask.resume()
}
and this is my Suggestion struct
struct Suggestion:Decodable{
var id: Int
var hometeam: String
var awayteam: String
var hometeamLogo: String
var awayteamLogo: String
var bet: String
var value: Double
var stake: Int
var bookieOdds: Double
}
and this is an example of what I receive from my api
[
{
"id": 132,
"hometeam": "Afjet Afyonspor",
"awayteam": "Sancaktepe Belediyespor",
"hometeamLogo": "https://media.api-football.com/teams/3565.png",
"awayteamLogo": "https://media.api-football.com/teams/3604.png",
"bet": "2",
"value": 1.19,
"stake": 1,
"bookieOdds": 2.55
},
{
"id": 152,
"hometeam": "BolĂ­var",
"awayteam": "Blooming",
"hometeamLogo": "https://media.api-football.com/teams/3702.png",
"awayteamLogo": "https://media.api-football.com/teams/3701.png",
"bet": "2",
"value": 3.18,
"stake": 1,
"bookieOdds": 11
}
]
You should better show the whole message precisely:
Member 'success' in 'Result<[Suggestion], SuggestionError>' produces
result of type 'Result<Success, Failure>', but context expects
'Result<[Suggestion], SuggestionError>'
And it is obviously wrong as you are passing a type name here:
completion(.success(Suggestion))
But before the line, you have one thing wrong:
let suggestionReponse = try decoder.decode(Suggestion.self, from: jsonData)
The outermost structure of your JSON response is an array ([ ... ]) and you need to decode it as an Array of Suggestions, not a Suggestion:
Please try something like this:
func getSuggestions(completion: #escaping(Result<[Suggestion], SuggestionError>) -> Void){
let dataTask = URLSession.shared.dataTask(with: resourceURL) { data, response, error in
guard let jsonData = data else{
completion(.failure(.noDataAvailable))
return
}
print(response!)
do{
let decoder = JSONDecoder()
//### Use `[Suggestion].self`, not `Suggestion.self`
let suggestions = try decoder.decode([Suggestion].self, from: jsonData)
completion(.success(suggestions)) //###Use the response, not the type name
} catch {
print(error) //### This gives you some hints on how to solve the error cases
completion(.failure(.canNotProcessData))
}
}
dataTask.resume()
}

Swift JSON parsing

I am working on an iOS weather app and need some help getting some values with JSON. I am trying to pull the value of id in the weather object. I am getting a value of nil when I debug. Could someone please help me with the logic? Here is the JSON request and Swift code.
{
"coord":{
"lon":138.93,
"lat":34.97
},
"weather":[
{
"id":800,
"main":"Clear",
"description":"clear sky",
"icon":"01n"
}
],
"base":"cmc stations",
"main":{
"temp":292.181,
"pressure":1005.21,
"humidity":100,
"temp_min":292.181,
"temp_max":292.181,
"sea_level":1014.59,
"grnd_level":1005.21
},
"wind":{
"speed":3.41,
"deg":78.0005
},
"clouds":{
"all":0
},
"dt":1464801034,
"sys":{
"message":0.003,
"country":"JP",
"sunrise":1464723086,
"sunset":1464774799
},
"id":1851632,
"name":"Shuzenji",
"cod":200
}
Here is my Swift snippet:
let requestURL: NSURL = NSURL(string: "http://api.openweathermap.org/data/2.5/weather?lat=35&lon=139&appid=6361e893fa064b1bfeaca686cd0929cc")!
let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
print("JSON Downloaded Sucessfully.")
do{
let json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)
if let today = json["weather"] as? [[String: AnyObject]] {
//this is pulling 4 key value pairs
for weather in today {
let id = weather["id"] as? String
self.trumpDescription.text=id;
print(id)
}
}
}
catch {
print("Error with Json: \(error)")
}
}
}
task.resume()
}
The problem lies in your cast to a string. If you do not cast the id as a String, it will print "800" as an Int.
When you are grabbing the id from the json and converting it to a string, it reads the string and includes the new line character.
I played around in the playground
So you are correctly getting to the spot in the json, but you need to unwrap your optional.
You can also use the nil coalescing operator to unwrap optionals so you don't have to have a bunch of if lets:
let id = weather["id"] as? String ?? ""
Which will either set id to the value or to "" if it doesn't exist.
try this in your code:
let id = weather["id"]?.stringValue
instead of this:
let id = weather["id"] as? String
And see the magic!
Edit for explanation:
As this answer worked for you, let me tell you why it did.
The id is being sent as integer from the server side. When you do weather["id"] it returns object of type AnyObject?. When you do weather["id"] as? String the casting fails thus you were getting nil.

How to parse this specific JSON data in Swift 2.0

I'm trying to parse Json Data from an API :
{
"title": "Mr. Robot",
"first_aired": "2015-06-24",
"network": "USA Network",
"channels": [
{
"id": 12,
"name": "USA",
"short_name": "usa",
"channel_type": "television"
}
],
The Code I'm use is:
var TVArray : [TVInfo] = []
var task : NSURLSessionTask?
func getJSON (urlString: String) {
let url = NSURL(string: urlString)!
let session = NSURLSession.sharedSession()
task = session.dataTaskWithURL(url) {(data, response, error) in
dispatch_async(dispatch_get_main_queue()) {
if (error == nil) {
self.updateJSON(data)
}
else {
}
}
}
task!.resume()
}
func updateJSON (data: NSData!) {
let JSONData = (try! NSJSONSerialization.JSONObjectWithData(data, options: []))
TVArray.removeAll(keepCapacity: true)
if let jsonArray = JSONData {
for j in jsonArray {
let title = jsonResult["title"] as! String
let firstAired = jsonResult["first_aired"] as! String
let network = jsonResult["network"] as! String
let channelName = JsonResult["channels"][0]["name"] as! String
let TV = TVInfo(title: title, firstAired: firstAired, network: network, channelName: channelName)
TVArray.append(TV)
}
}
collectionview.reloadData()
}
}
When I use the above code I get an error 'Initializer for conditional binding must have Optional type, not 'AnyObject'' in front of the line 'if let jsonArray = JsonData'. I've tried some methods I've seen on StackOverflow like the method in the link :
[Parsing JSON in swift 2.0
but it didn't work for me. I'm still a bit new to Swift, I really don't want to use SwiftyJSON. Is this the best way to parse this JSON data or is there a better way of doing it?
Since you've used NSJSONSerialization with try! (note the !, meaning it was forced) the value of JSONData is not an optional: you don't have to unwrap it with if let jsonArray = JSONData.
If you still want an optional value, use try? instead.
Otherwise you could also use try inside a do catch block to handle possible errors.
The type of JSONData is unknown, it needs to be known to be an Array for the following for loop.
use:
let JSONData = try! NSJSONSerialization.JSONObjectWithData(data!, options:[]) as! NSArray
You do not need:
if let jsonArray = JSONData {
because you have already crashed if JSONData is nil from the preceding try!
You are better with:
do {
let jsonArray = try NSJSONSerialization.JSONObjectWithData(data!, options:[]) as! NSArray
for j in jsonArray {
// ...
}
} catch {
// handle error
}
Because you have no control over the JSON you receive and crashing because of a server change is not a good idea.
Also really put some time into naming variables, JSONData is not data, it is an array obtained by parsing a JSON string.