How to decode this particular JSON model? - ios

EDIT 2:
After changing the Model struct and calling the JSON decoder without a completion handler, I have managed to get this to work. Thank you all for your help.
Fixed code:
Model
import Foundation
struct RatesResponse: Decodable, Hashable{
let rates: [String: Double]
}
Decoder Class
import Foundation
class RatesModelData{
public var rateCurrency = [String]()
public var rateValue = [Double]()
public func getRates(currency: String){
guard let url = URL(string: "https://api.exchangerate.host/latest?base=\(currency)")
else {
print("URL is invalid")
return
}
var request = URLRequest(url: url)
let dataTask = URLSession.shared.dataTask(with: request){data, response, error in
if let data = data {
do{
let ratesResponse = try JSONDecoder().decode(RatesResponse.self, from: data)
for rate in ratesResponse.rates{
self.rateCurrency.append(rate.key)
self.rateValue.append(rate.value)
}
print(self.rateCurrency)
} catch {
print(error)
}
}
}
dataTask.resume()
}
}
EDIT:
I have changed let rate = Rates to let rates = Rates, modified the decoder class to the following and added a do/catch statement however I am now getting the error "The data given is invalid JSON". I have updated the code snippets as well.
I have this json model:
https://api.exchangerate.host/latest?base=usd
and I cannot for the life of me figure out how to decode it properly. I know its a dictionary and has to be decoded as such but i'm struggling to wrap my head around what i'm doing wrong.
Model:
struct RatesResponse: Decodable, Hashable{
let rates : Rates
}
struct Rates: Decodable, Hashable {
let rates: [String: Double]
}
Decoder Class:
class RatesModelData{
public func getRates(currency: String, _ completionHandler: #escaping(Rates) -> Void){
guard let url = URL(string: "https://api.exchangerate.host/latest?base=\(currency)")
else {
print("URL is invalid")
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "ACCEPT")
let dataTask = URLSession.shared.dataTask(with: request){data, response, error in
do{
if let data = data {
if let ratesResponse = try JSONDecoder().decode(RatesResponse?.self, from: data){
completionHandler(ratesResponse.rates)
}
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else{
print("Error with response: \(response)")
return
}
if let error = error {
print("Error Thrown : \(error)")
return
}
} catch {
print(error)
}
}
dataTask.resume()
}
}
I ideally want to get this to be displayed in a list view (SwiftUI), but for now just asking for some advice as to where i've gone wrong with my decoding.

Your object model suggests that the value associated with rates key is another object with another rates key. But that doesn’t match the JSON you have provided. The value associated with top-level rates key is just a dictionary.
So you could do the following and be done with it:
struct ResponseObject: Decodable {
let rates: [String: Double]
}
Or, if you want to capture everything in this JSON, you could add the additional properties:
struct Motd: Decodable {
let msg: String
let url: URL
}
struct ResponseObject: Decodable {
let motd: Motd
let success: Bool
let base: String
let date: Date
let rates: [String: Double]
}
And then:
do {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
let responseObject = try decoder.decode(ResponseObject.self, from: data)
print(responseObject)
} catch {
print(error)
}
FWIW, the base, date, and rates objects likely could/should be optionals, e.g.:
struct ResponseObject: Decodable {
let motd: Motd
let success: Bool
let base: String?
let date: Date?
let rates: [String: Double]?
}
To confirm, we would need to see what a well-formed non-success response looks like. Glancing at the documentation it was not immediately obvious, but something like the above is likely what you want, where JSONDecoder would be able to successfully decode both success responses and failure responses.

I went through the struggles of parsing JSON into structs last year so I feel comfortable helping you out. It's definitely a challenging concept to wrap your head around:
First, let me explain what's wrong with your structs:
struct RatesResponse: Decodable, Hashable{
let rates : Rates
// "rates" is a root-level key in your JSON.
// You're giving it the value of your structure Rates,
// which also has a key titled "rates".
}
struct Rates: Decodable, Hashable {
let rates: [String: Double]
}
Here's what your JSON would look like if these structs could be used to parse it:
{
"rates": {
"rates": {
"AED": 3.671827,
"AFN": 80.098152
}
}
In your actual JSON, your "rates" key has the value of an object (or dictionary, which is nothing more than an array with keys and values) I'm sure you don't want to make a struct with a variable for every single currency, so just use an array in your struct, with an element type of String: Double
struct RatesResponse: Decodable, Hashable{
let rates : [String: Double]
}
JSON key "rates" has a value of a dictionary where the keys are strings and the values are doubles.
After decoding in your URLSession, you can do:
for rate in ratesResponse.rates {
let currency = rate.key // AED, AFN, etc.
let exchangeRate = rate.value // 3.671827, 80.098152, etc.
// With these variables, you can append each of them to a new, separate array for use in your SwiftUI list.
}

Related

Fetching and parsing JSON

I’m having a really hard time fetching and parsing the following JSON. I can't even fetch the data from given url, even less parse it using my data Model "Car". Any help is more than welcomed!
JSON
{
"cars":[
{
"date_stolen":1604616183,
"description":null,
"body_colors":[
"Black",
"Blue"
],
"id":"944846",
"is_stock_img":false,
"large_img":null,
"location_found":null,
"manufacturer_name":"Toyota",
"external_id":null,
"registry_name":null,
"registry_url":null,
"serial":"36-17-01012-xl09",
"status":null,
"stolen":true,
"stolen_location":"Calgary - CA",
"thumb":null,
"title":"2017 Toyota Corolla ",
"url":"https://cars.org/944846",
"year":2017
}
]
}
struct Car: Decodable {
let cars: String
}
var cars = [Car]()
fileprivate func fetchJSON() {
let urlString = "someUrl…"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, _, err) in
DispatchQueue.main.async {
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
print("DATA \n", data)
self.cars = try decoder.decode(Car.self, from: data)
print("---> ", data)
} catch let jsonErr {
print("Failed to decode:", jsonErr)
}
}
}.resume()
}
You Car model does not represent you json structure.
You're looking for something like this :
struct CarsResponse: Decodable {
let cars: [CarDTO]
}
struct CarDTO: Decodable {
let id: String
let isStockImg: Bool
// And so on
}
Then just write :
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let carResponse = try decoder.decode(CarsResponse.self, from: data)
You will access your cars by writing
let cars: [CarDTO] = carResponse.cars
If you're going to use the Codable Protocol and JSONDecoder() you will need to create a Struct that matches the format of your JSON. As others have pointed out in their comments, your Car struct is completely different than the format of the data you are receiving.
If you just want to deserialize the JSON you are receiving into dictionaries containing values, you could instead use JSONSerialization.
To do that you could replace the body of your do block with code like this:
let object = try JSONSerialization.jsonObject(with: data, options: [])
You'd then have to navigate the dictionary structure of object yourself. It isn't as clean as creating a custom struct and using Codable, but it works.
Note that your JSON has syntax errors in it. Here is a cleaned-up version:
{"cars":
[
{"date_stolen":1604616183,
"description":null,
"body_colors":["Black","Blue"],
"id":944846,
"is_stock_img":false,
"large_img":null,
"location_found":null,
"manufacturer_name":"Toyota",
"external_id":null,
"registry_name":null,
"registry_url":null,
"serial":"36-17-01012-xl09",
"status":null,
"stolen":true,
"stolen_location":"Calgary - CA",
"thumb":null,
"title":"2017 Toyota Corolla ",
"url":"https://cars.org/944846",
"year":2017
}
]
}
(I added whitespace for readability. It doesn't affect the parsing one way or the other.)

Chaining Multiple JSON Request Using Decodable - Swift 5

My "url" object has a link that captures what the user types into a search bar to complete the link then begin the JSON process. The first link responds with another link after the JSON has finished parsing. In my if let validLink = result.link you see I store the link information into an Array. Now I'm not sure if I should begin another JSON response in my if let validLink = result or if I should create a new function like I'm attempting to do in the code below and basically copied and pasted the same JSON information below to reparse it. The second link is getting a parse error. What is the most efficient and right way to do this? I'm really stuck here.
I've tried to create another function that uses the information from the first JSON parse to reparse again using the new link.
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
if let searchText = searchController.searchBar.text, !searchText.isEmpty {
let url = URL(string: "http://djp-dev/api/item?q=\(String(describing: searchText))&dev=1")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
guard let data = data,
error == nil else {
print(error?.localizedDescription ?? "Response Error")
return }
do {
let jsonResult = try JSONDecoder().decode(Response.self, from: data)
let resultsArray = jsonResult.results
for result in resultsArray {
if let validLink = result.link {
print(validLink)
self.collectLink.append(validLink)
self.mainParse()
}
}
} catch {
print("Parse Error")
}
}
task.resume()
}
}
func mainParse() {
let url = URL(string: "http://djp-dev\(collectLink[0])?dev=1")
print(url!)
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
guard let data = data,
error == nil else {
//print(error?.localizedDescription ?? "Response Error")
return }
do {
let jsonResult = try JSONDecoder().decode(JSONResponse.self, from: data)
let mainArray = jsonResult.locations
for main in mainArray {
print("""
Manufacture = \(main.rid)
Description = \(main.description)
""")
/*if let validLink = result.description! {
}*/
}
} catch {
print("Parse Error")
}
}
task.resume()
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
I basically ask http://djp-dev/api/item?q=\(String(describing: searchText))&dev=1 for a link in the response it sends me back. I want the use the link it sends me to start another JSON request. I'm not sure if I should keep it all into one request or create a whole new function with a whole new JSON request. if let validLink = result.link { } is when I receive the second link information.
So I've figured it out. I was using Decodable without CodingKeys. Thanks to Vadian for pointing me in the right direction. Here's my example:
struct Response : Decodable {
let results: [Results]
enum CodingKeys: String, CodingKey {
case results = "photos"
}
}
struct Results : Decodable {
let url : String?
enum CodingKeys: String, CodingKey {
case url = "url"
}
}
What #vadian is saying is, Codable autmagically uses variable names as a coding key. So you can simply add Decodable like:
struct Response: Decodable {
let results: [Results]
private enum CodingKeys: String, CodingKey {
case results = "photos"
}
}
struct Results: Decodable {
let url: String?
}
and if you change the name of results to photos, you can do
struct Response: Decodable {
let photos: [Results]
}
struct Results: Decodable {
let url: String?
}
On the contrary, if you need a post-processing of data, e.g., converting a String to Date, you need to implement init(from decoder: Decoding) throws yourself. I strongly recommend reading Encoding and decoding custom types.
If you have a sample json, there are many free online tools available to create custom swift structs/classes with codable protocol
one such example is https://app.quicktype.io

Getting objects from JSON

My problem:
I use the site API - https://www.themealdb.com/api.php .
I want to get a list of all products. For this purpose, the link is https://www.themealdb.com/api/json/v1/1/categories.php
In my code, I created a structure:
struct Category: Decodable {
var idCategory: Int?
var strCategory: String?
var strCategoryDescription: String?
var strCategoryThumb: String?
}
Then I try to get to the address and get the data. I can convert the incoming data to JSON. It works.
Next, I want to convert the data and write it into an array of structures.
func load(url: String, completion: #escaping (_ objects: [Category])->()) {
guard let url = URL(string: url) else { return }
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
//let json = try? JSONSerialization.jsonObject(with: data, options: [])
//print("JSONSerialization" + "\(json)")
let object = try JSONDecoder().decode([Category].self, from: data)
print("JSONDecoder" + "\(object)")
completion(object)
} catch {
print(error.localizedDescription)
}
}.resume()
}
But in this line I get an error in the console:
The data couldn’t be read because it isn’t in the correct format.
Probably a mistake in my structure. I can not deal with this problem.
There are two mistakes.
The actual error
Type 'Array' mismatch: Expected to decode Array but found a dictionary instead.
indicates that you are ignoring the root object, the dictionary with key categories
The value for key id is String not Int, note the double quotes in the JSON
Declare all struct members as non-optional constants as the JSON provides all keys the in dictionaries. And please map the horrible dictionary keys to more meaningful member names.
And print all errors and never .localizedDescription in a Decodable catch block.
struct Response: Decodable {
let categories: [Category]
}
struct Category: Decodable {
let id: String
let name: String
let description: String
let thumbnailURL: URL
private enum CodingKeys: String, CodingKey {
case id = "idCategory"
case name = "strCategory"
case description = "strCategoryDescription"
case thumbnailURL = "strCategoryThumb"
}
}
func load(url: String, completion: #escaping ([Category]) -> Void) {
guard let url = URL(string: url) else { return }
let session = URLSession.shared
session.dataTask(with: url) { (data, _, error) in
if let error = error { print(error); return }
do {
let response = try JSONDecoder().decode(Response.self, from: data!)
print("JSONDecoder", response)
completion(response.categories)
} catch {
print(error)
completion([])
}
}.resume()
}
You need two codables
struct MyData: Codable {
var categories: [Category]?
}
And
let object = try JSONDecoder().decode(MyData.self, from: data)
With a wrapper class you can fetch your categories. The following code works fine in Playground:
let json = """
{
"categories": [
{"idCategory": "1"},
{"idCategory": "2"}
]
}
"""
struct CategoryHolder: Codable {
var categories: [Category]
}
struct Category: Codable {
let idCategory: String?
let strCategory: String?
let strCategoryDescription: String?
let strCategoryThumb: String?
}
let jsonData = Data(json.utf8)
let categories = try JSONDecoder().decode(CategoryHolder.self, from: jsonData).categories

Passing JSON result into a struct model

I am receiving a result from an API, I can iterate through the result. My understanding is I can pass the value into a model immediately.
Apple Developer article on struct models
My issue is I am not doing it properly and am receiving a nil value. Perhaps someone can see where I need to change. I am using Swift 4.2
Here is my struct model.
import Foundation
struct ProfileModel {
//MARK: Properties
var name: String
var email: String
var profileURL: String
//MARK: Initialization
}
extension ProfileModel{
init?(json: [String:AnyObject]) {
guard
let name = json["name"] as? String,
let email = json["email"] as? String,
let profileURL = json["profileURL"] as? String
else { return nil }
self.name = name
self.email = email
self.profileURL = profileURL
}
}
Here is my result code from my urlConnection. Let me know if we want to see the entire swift file
//create dataTask using the session object to send data to the server
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
//create json object from data
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:AnyObject] {
self.onSuccess(data: json)
}
} catch let error {
print(error.localizedDescription)
}
})
task.resume()
}
func onSuccess(data: [String:AnyObject]){
print("onSuccess")
let myProfile = ProfileModel(json: data)
//myProfile is nil while unwrapping
let title: String = myProfile!.name
print(title)
}
I could just iterate through the strings since I am able to print 'data'. I just figured it would be cleaner to put everything into a ProfileModel and manage that object as a whole.
This json is my more simple one which is why I used it for this question. I also can't remember but I had to use "[String:AnyObject]" to get the json properly. This was pulled directly from my terminal, this was the data being passed in my JsonResponse. The output json from Xcode has [] on the outside instead.
{
'detail': 'VALID',
‘name’: ‘Carson,
'email': ‘carson.skjerdal#somethingelselabs.com',
'pic_url': None
}
EDIT:
So my problem is solved, and ultimately moving to Codable was the key. Here is my fixed code for anyone who might need a working solution.
URLSession.shared.dataTask(with: request as URLRequest) { (data, response
, error) in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(ProfileModel.self, from: data)
print(gitData.name)
self.onSuccess(data: gitData)
} catch let err {
print("Err", err)
}
}.resume()
}
func onSuccess(data: ProfileModel){
print("onSuccess")
print(data.email)
}
My Codable Struct - slightly simplified
import Foundation
struct ProfileModel: Codable {
let detail, name, email: String
private enum CodingKeys: String, CodingKey {
case detail, email
case name = "firstname"
//case picUrl = "pic_url"
}
}
After "Codable" has been introduced I always uses that.
You can take your JSON ans pars it in to QuickType.io, and you will get a Struct that confirms to the codadable
// To parse the JSON, add this file to your project and do:
//
// let aPIResponse = try? newJSONDecoder().decode(APIResponse.self, from: jsonData)
import Foundation
struct APIResponse: Codable {
let detail, name, email, picUrl: String
enum CodingKeys: String, CodingKey {
case detail, name, email
case picUrl = "pic_url"
}
}

Array of Codable structs possibly decoded from JSON data object

I have this piece of code:
struct NoteRecord: Codable {
let id: String
let title: String
let detail: String?
let dueDate: String?
private enum CodingKeys: String, CodingKey {
case id, title, detail, dueDate
}}
and parsing part:
do {
let decoder = JSONDecoder()
let note = try decoder.decode(NoteRecord.self, from: data)
} catch let err {
print("Error occured:", err)
}
Is there any way to use this when REST API returns an array of objects to decode the data correctly as array of structs?
Yes, just use this:
do {
let decoder = JSONDecoder()
let notes = try decoder.decode([NoteRecord].self, from: data)
} catch let err {
print("Error occured:", err)
}
If you use [YourCodableStruct].self you are parsing the array. If you use YourCodableStruct.self you are parsing the struct.
You can implement another struct to hold the array.
struct NoteRecords: Codable {
var list: [NoteRecord] // You should change the var name and coding keys
}
And parse it like
let note = try decoder.decode(NoteRecords.self, from: data)
I hope this helps.

Resources