Chaining Multiple JSON Request Using Decodable - Swift 5 - ios

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

Related

iOS: How to access and use API response data from REST SwiftUi

firstly I am really new to iOS development and Swift (2 weeks coming here from PHP :))
I am creating a application that has a callout to my very simple api. I am using the below code but an unable to access the 'if let response' part of the code and unable to get the value of 'Comments' from the returned data from api. Wondering if anyone can help on this.
The data come back fine as
//Print out fine
print(str)
will print my response of {"Comments":"success"}
so just wondering how I can properly use this data and check if success etc
Thanks
func loadData() {
guard let url = URL(string: "myapi.php") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
print("hello1")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
print("hello2")
let str = String(decoding: data, as: UTF8.self)
//Print out fine
print(str)
// Cannot enter this if statement
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
print("hello3")
print(response)
}
return
}
}
}.resume()
}
struct TaskEntry: Codable {
public var Comments: String
}
With the description provided by you, the JSON that returns your API is the following:
{
"Comments": "success"
}
Is that right?
If it is, this part of your code
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data)
Marks that you are decoding data fetched from your app as Array. You specify an array of TaskEntry right here [TaskEntry].self for your data that's not true, you need to decode json with try? JSONDecoder().decode(TaskEntry.self, from: data)
If you hope for an array your api must to return something like this
[
{
"Comments": "success"
}
]
For check if it is right with status code I provide a restructuration of your code explained.
First, protocol Codable it's used to decode and encode data with Encoders and Decoders. In that case, you are working with JSONs so you use JSONDecoder to decode data. You only need decode, so with Decodable protocol It's enough. You don't need to mark Comments with public, because you are working with TaskEntry in your project, so by default (no specified) It's internal. Moreover you don't need var, because, you are not making changes of the property.
Also, properties as standard starts with lowercase, so you can use CodingKeys to keep a quality code standard.
You can check it in Apple Developer site
struct TaskEntry: Decodable {
let comments: String
enum CodingKeys: String, CodingKey {
case comments = "Comments"
}
}
Then, you should to think about your Api errors and defined it. I give you an example:
enum NetworkErrors: Error {
case responseIsNotHTTP
case noDataProvided
case unknown
}
Your load function, should to communicate to external class or struct the TaskEntry or the error. You could use a closure with a Result. Please check the link Hacking with Swift. This has two cases with associated values, failure and success.
Further the response of your UrlSessionTask need to be cast to HTTPURLResponse to get the response http status code. I provide an example of that with your code.
func loadData(result: #escaping (Result<TaskEntry, Error>) -> Void) {
guard let url = URL(string: "myapi.php") else {
result(.failure(URLError(.badURL)))
return
}
let request = URLRequest(url: url) // your method is Get, if not you need to set the http method of the request
URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response as? HTTPURLResponse else {
if let error = error {
print(error.localizedDescription)
result(.failure(error))
} else {
result(.failure(NetworkErrors.responseIsNotHTTP))
}
return
}
switch response.statusCode {
case 200...299:
if let data = data {
do {
let taskEntry = try JSONDecoder().decode(TaskEntry.self, from: data)
result(.success(taskEntry))
} catch {
result(.failure(error))
}
} else {
result(.failure(NetworkErrors.noDataProvided))
}
default:
if let error = error {
print(error.localizedDescription)
result(.failure(error))
} else {
result(.failure(NetworkErrors.unknown))
}
}
}.resume()
}
Additionally, you could pass this code to async/await form with a new function in iOS 15 or MacOS 12 (async/await it's a feature of swift 5.5, but the urlsession function that i've used it's only from ios 15 and macos 12 to up):
#available(macOS 12.0, iOS 15.0, *)
func loadData() async throws -> Result<TaskEntry,Error> {
guard let url = URL(string: "myapi.php") else {
throw URLError(.badURL)
}
do {
let (data, urlResponse) = try await URLSession.shared.data(from: url, delegate: nil)
guard let response = urlResponse as? HTTPURLResponse else {
return .failure(NetworkErrors.responseIsNotHTTP)
}
switch response.statusCode {
case 200...299:
do {
let taskEntry = try JSONDecoder().decode(TaskEntry.self, from: data)
return .success(taskEntry)
} catch {
return .failure(error)
}
default:
return .failure(NetworkErrors.unknown)
}
} catch {
return .failure(error)
}
}
Please, vote up if it's clarify you.
Have a good day :)

Swift struct optional values

I am adding a simple login system to my SwiftUI project. Only I can't quite figure it out.
What the problem is, when a user wants to login and it works. I get this response from the server:
"user": {
"id": 6,
"name": "test",
"email": "test#test.com",
"email_verified_at": null,
"created_at": "2020-07-02T09:37:54.000000Z",
"updated_at": "2020-07-02T09:37:54.000000Z"
},
"assessToken": "test-token"
}
But when something isn't right, the server displays an error message like this:
"message": "The given data was invalid.",
"errors": {
"email": [
"The email field is required."
],
"password": [
"The password field is required."
]
}
}
How can I make sure I parse this information into a structure. At the moment it looks like this.
// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
// let welcome = try? newJSONDecoder().decode(Welcome.self, from: jsonData)
import Foundation
// MARK: - Welcome
struct Login: Codable {
let user: User
let assessToken: String
}
// MARK: - User
struct User: Codable {
let id: Int
let name, email: String
let emailVerifiedAt: JSONNull?
let createdAt, updatedAt: String
enum CodingKeys: String, CodingKey {
case id, name, email
case emailVerifiedAt = "email_verified_at"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
This is how i do it now:
class HttpAuth: ObservableObject{
var didChange = PassthroughSubject<HttpAuth, Never>()
var authenticated = false{
didSet{
didChange.send(self)
}
}
func checkDetails(email: String, password: String){
guard let url = URL(string: "https://test.ngrok.io/api/login") else {
return
}
let body : [String : String] = ["email" : email, "password": password]
let finalBody = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = finalBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
let finalData = try! JSONDecoder().decode(Login.self, from: data)
print(finalData)
}.resume()
}
}
Do I have to create a new struct named like LoginError for example, or do I need it inside the existing login struct?
You need to create separate Codable models for both the success and error cases. And then combine them into a single model that you can use for parsing.
Login model:
struct Login: Decodable {
let user: User
let assessToken: String
}
struct User: Decodable {
let id: Int
let name, email: String
let emailVerifiedAt: String?
let createdAt, updatedAt: String
}
Error model:
struct ErrorResponse: Decodable {
let message: String
let errors: Errors
}
struct Errors: Decodable {
let email, password: [String]
}
Combine the Login and ErrorResponse models into Response like so,
enum Response: Decodable {
case success(Login)
case failure(ErrorResponse)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let user = try container.decode(Login.self)
self = .success(user)
} catch {
let error = try container.decode(ErrorResponse)
self = .failure(error)
}
}
}
Now, use Response model to parse your data like so,
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try decoder.decode(Response.self, from: data)
switch response {
case .success(let login):
let assessToken = login.assessToken
print(assessToken)
//get any required data from login here..
case .failure(let error):
let message = error.message
}
} catch {
print(error)
}
}.resume()
Or use SwiftyJSON.
If third-party dependency is your concern. Write one yourself.
SwiftyJSON is basically one struct, namely JSON, to refactor out common operations.
The idea behind is simple:
JSON data is dynamic, Codable is largely static.
You don't usually have the luxury of immutable JSON response, hell, API itself changes all the time during development.
You also need to create Codable structs recursively with extra handling of special coding keys.
Codable only makes sense when in small scale or it's auto-gen.
Take another answer for example, you need to define 5 types for a JSON response. Most of them are not reusable.
With JSON, it is var j = JSON(data).
guard let user = j[.user].string else {// error handling} ...
where you replace string user with an enum case case user.
JSON, is reusable, coding key .user is reusable.
Since JSON is nested, you can replace user.id with j[.user][.id].stringValue or j[.user][.id].intValue depending on use case.
And again .user, .id can be added to coding keys enum to be reusable.
You also have great flexibility to adjust to run-time variations.
Ironically, in my opinion, one should use Codable for Encodable, not for Decodable.
Because when you encode, you have full control of its type; when you decode json response, you are at the mercy of backend.
Swift type system is great, but you don't always need the exact type at every step.
JSON is invaluable in deeply nested json response, e.g.; j[.result][.data][0][.user][.hobbies][0][.hobbyName].stringValue. When I'm done, you are still writing first level Codable struct.
Share this here as a comparison so more could appreciate how insanely powerful this is.

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"
}
}

JSONDecoder Not Parsing Data

I am trying to get data from this URL
https://api.opendota.com/api/heroStats
I have made a struct
struct HeroStats : Decodable {
let localized_name: String
let primary_attr: String
let attack_type: String
let legs: Int
let image: String
}
Top of my View Controller
var heros = [HeroStats]()
func downloadJSON(completed: #escaping () -> ()) {
let url = URL(string: "https://api.opendota.com/api/heroStats")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print(error.debugDescription)
}
do {
guard let data = data else { return}
self.heros = try JSONDecoder().decode([HeroStats].self, from: data)
DispatchQueue.main.async {
completed()
}
print(self.heros)
} catch {
print("JSON ERROR")
return
}
}.resume()
}
I always return JSON ERROR for some reason, although everything seems to be correct.
Try to read more on Codable/Encodable in Swift
Encoding and Decoding custom types
You may want to improve your code by making Swift names that differs from JSON names
struct HeroStats: Codable {
let name: String
let primaryAttribute: String
let attackType: String // Better to be an enum also
let legs: Int
let image: String?
enum CodingKeys: String, CodingKey {
case name = "localized_name"
case primaryAttribute = "primary_attr"
case attackType = "attack_type"
case legs
case image = "img"
}
}

Resources