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"
}
}
Related
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.
}
When I make an api request through urlSession to the server I might get a json like
{
"movie": "Avengers",
"director": "Joss Whedon",
}
or like this
{
"apiKey": "invalid"
}
I have two structs to save like this
struct Movie: Codable {
let request: String
let director: String
init(movie:String, director:Sring) {
self.movie = movie
self.director = director
}
}
struct Valid: Codable {
let apiKey: String
init(apiKey:String) {
self.apiKey = apiKey
}
}
Based on the response i want to either decode to first struct or the second struct. How to do that.
if let url = URL(string: "myurl.com") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
guard let json = data as? [String:AnyObject] else {
return
}
do {
if let movie = json["movie"]{
//moview is there. so you can decode to your movie struct
let res = try JSONDecoder().decode(Movie.self, from: data)
}else{
//move is not there.it means error
let res = try JSONDecoder().decode(Valid.self, from: data)
}
} catch let error {
print(error)
}
}
}.resume()
}
Parse json with a single Struct like this
struct RootClass : Codable {
let apiKey : String?
let director : String?
let movie : String?
enum CodingKeys: String, CodingKey {
case apiKey = "apiKey"
case director = "director"
case movie = "movie"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
apiKey = try values.decodeIfPresent(String.self, forKey: .apiKey)
director = try values.decodeIfPresent(String.self, forKey: .director)
movie = try values.decodeIfPresent(String.self, forKey: .movie)
}
}
And check the value of key apiKey if it's nil then use movies and director. or use the value of apiKey
First of all, the Codable types to parse the above 2 JSON responses should look like,
struct Movie: Decodable {
let movie: String
let director: String
}
struct Valid: Decodable {
let apiKey: String
}
There is no need to explicitly create init(). Codable will handle that automatically.
Next, you need to create another Codable type that can handle both the responses, i.e.
enum Response: Decodable {
case movie(Movie)
case valid(Valid)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let data = try container.decode(Movie.self)
self = .movie(data)
} catch {
let data = try container.decode(Valid.self)
self = .valid(data)
}
}
}
Now, you can parse the JSON data like so,
do {
let response = try JSONDecoder().decode(Response.self, from: data)
print(response)
} catch {
print(error)
}
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
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
if let url = URL(string: "https://mysit.com") {
URLSession.shared.dataTask(with: url) {
data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let data = data, error == nil,
let valueEncoding = response?.textEncodingName,
let getContent = String(data: data, encoding: valueEncoding.textEncodingToStringEncoding)
else { return }
print(getContent)
}.resume()
}
my Data
{"Regions":null,"Cities":[{"Id":"9605","Name":"YANBAA AS SENAYAH"},{"Id":"15","Name":"ABHA"},{"Id":"13","Name":"AD DAMMAM"},{"Id":"1542","Name":"AL BAHA"},{"Id":"14","Name":"AL MADINAH AL MUNAWWARAH"},{"Id":"2213","Name":"AR'AR"},{"Id":"11","Name":"BURAYDAH"},{"Id":"10","Name":"HAIL"},{"Id":"17","Name":"JAZAN"},{"Id":"6","Name":"MAKKAH AL MUKARRAMAH"},{"Id":"3417","Name":"NAJRAN"},{"Id":"3","Name":"RIYADH"},{"Id":"2237","Name":"SAKAKA"},{"Id":"1","Name":"TABUK"},
how to get an array list of values "Name" ,can you help me?
You can try
struct Root :Decodable{
let Cities:[InnerItem]
}
struct InnerItem :Decodable{
let Id:String
let Name:String
}
do {
let arr = try JSONDecoder().decode(Root.self, from: data)
print(arr.Cities)
}
catch {
print(error)
}
//
Note : This is the correct json structure
{"Regions":null,"Cities":[{"Id":"9605","Name":"YANBAA AS SENAYAH"},{"Id":"15","Name":"ABHA"},{"Id":"13","Name":"AD DAMMAM"},{"Id":"1542","Name":"AL BAHA"},{"Id":"14","Name":"AL MADINAH AL MUNAWWARAH"},{"Id":"2213","Name":"AR'AR"},{"Id":"11","Name":"BURAYDAH"},{"Id":"10","Name":"HAIL"},{"Id":"17","Name":"JAZAN"},{"Id":"6","Name":"MAKKAH AL MUKARRAMAH"},{"Id":"3417","Name":"NAJRAN"},{"Id":"3","Name":"RIYADH"},{"Id":"2237","Name":"SAKAKA"},{"Id":"1","Name":"TABUK"}]}
let responseData = try JSONSerialization.jsonObject(with: (response["Cities"] as! String).data(using: String.Encoding.utf8)!, options: []) as! [[String: Any]]
for item in responseData{
let name = item["Name"] as! String
}
Together with the decoding step. I added several guards to print an error if one comes up. It is generally good practice to throw the error and handle it on the appropriate level.
func work() {
guard let url = URL(string: "https://mysit.com") else {
fatalError("url is nil.")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard error == nil else {
fatalError("\(error!)")
}
guard let response = response as? HTTPURLResponse,
response.statusCode == 200 else {
fatalError("Response is nil.")
}
guard let data = data else {
fatalError("data is nil.")
}
decode(data: data)
}.resume()
}
func decode(data: Data) {
let decoder = JSONDecoder.init()
let welcome = try! decoder.decode(Welcome.self, from: data)
print(welcome.cities.first!)
}
The decoding helpers. enum CodingKeys are used to convert the lowercase attributes to the uppercase JSON attributes and back.
struct Welcome: Codable {
var regions: [Region]?
let cities: [City]
enum CodingKeys: String, CodingKey {
case regions = "Regions"
case cities = "Cities"
}
}
struct City: Codable {
let id, name: String
enum CodingKeys: String, CodingKey {
case id = "Id"
case name = "Name"
}
}
struct Region: Codable {
let id, name: String
enum CodingKeys: String, CodingKey {
case id = "Id"
case name = "Name"
}
}
Some use services like Quicktype to convert JSON strings to the specific programming language. It makes things faster and simpler.