Writing a JSON serialization function - json

I'd like to make a function that takes in a few parameters and then outputs the data I need from a web API. Obviously a good deal of the time I'll need to customize it to suit the use case but just for fun I'm trying to figure out a super basic function the successfully parses JSON, as about half of the lines of code in the function below are generic error handling.
For example if I generally use something like
func getJSON(completionHandler: #escaping (Bool) -> ()) {
let jsonUrlString = "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808"
guard let url = URL(string: jsonUrlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data, err == nil else {
print(err!)
return
}
do {
let response = try
JSONDecoder().decode(TopStoriesResponse.self, from: data)
self.storyData = response.results
completionHandler(true)
} catch let jsonErr {
print("Error serializing JSON", jsonErr)
}
}.resume()
}
The only three things that will change from case to case (again, in the most absolutely basic of scenarios) are the url link to the API, the Struct that I set up to look for the pieces of data I need, and the array that I output the results to once the data request is finished.
Could I trim the fat on that and do something like
func jsonFetcher(apiLink: String, structToDecode: String, arrayThatHoldsResponse: [String], completionHandler: #escaping (Bool) -> ()) {
let jsonUrlString = apiLink
guard let url = URL(string: jsonUrlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data, err == nil else {
print(err!)
return
}
do {
let response = try
JSONDecoder().decode(structToDecode, from: data)
arrayThatHoldsResponse = response.results
completionHandler(true)
} catch let jsonErr {
print("Error serializing JSON", jsonErr)
}
}.resume()
}
I'm just not sure about the data types of structToDecode and arrayThatHoldsResponse (in the example function above I just using String as a placeholder), assuming they look like
Struct(s)
struct TopStoriesResponse: Decodable {
let status: String
let results: [Story]
}
struct Story: Decodable {
let title: String
let abstract: String
let url: String
let multimedia: [Multimedia]
private enum CodingKeys: String, CodingKey {
case title
case abstract
case url
case multimedia
}
init(from decoder:Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
abstract = try container.decode(String.self, forKey: .abstract)
url = try container.decode(String.self, forKey: .url)
multimedia = (try? container.decode([Multimedia].self, forKey: .multimedia)) ?? []
}
}
Array
var storyData = [Story]()
This way I can just call
jsonFetcher(apiLink: link, structToDecode: myStruct, arrayThatHoldsResponse: myArray, completionHandler: <#T##(Bool) -> ()#>)
Thanks for any help!

The power of generics. You can make a generic function, where the parameter is the urlString. The T inheritance the Decodable protocol.
This way you can call this function everytime as long as your Model inheritance the Decodable protocol.
func fetchData<T: Decodable>(urlString: String, completion: #escaping (T) -> ()) {
let url = URL(string: urlString)!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
}
guard let data = data else { return }
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(object)
} catch let jsonErr {
print("Failed to decode json:", jsonErr)
}
}.resume()
}
How to call the function:
struct User: Decodable { }
fetchData(urlString: "yourUrl") { (User: User) in
// Handle result
}
struct Animal: Decodable { }
fetchData(urlString: "yourUrl") { (animal: Animal) in
// Handle result
}
// Or if you want to fetch an array of users instead
fetchData(urlString: "yourUrl") { (users: [User]) in
// Handle result
}
In your case
var storiesData: [Story] = []
fetchData(urlString: "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808") { (stories: [Story] in
storiesData = stories
DispatchQueue.main.async {
self.tableView.reloadData()
}
}

Based on Jacob's answer I recommend to return also a possible error.
To keep the generic layout declare an – also generic – enum as return type
enum FetchResult<T> {
case success(T), failure(Error)
}
and return FetchResult with the passed static type
func fetchData<T: Decodable>(url: URL, completion: #escaping (FetchResult<T>) -> Void) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {completion(.failure(error!)); return }
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(.success(object))
} catch {
completion(.failure(error))
}
}.resume()
}
and use it
let jsonUrl = URL(string: "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=••••••••••••••••••:1:73741808")!
fetchData(url: jsonUrl) { (result : FetchResult<TopStoriesResponse>) in
switch result {
case .success(let object): print(object) // do something with object
case .failure(let error): print(error) // handle the error
}
}

Related

Swift - JSON decoding

I have a JSON response from an api call. The problem is I get different JSON responses depending on whether the user has entered the correct credentials or not. My question is how do I read and decode these responses to a useable struct and what is the best way to go about decoding these different responses. one thing I noticed is both response have a common "isSuccess" that may be useful. I have little to no experience with swift or reading JSON so this is all a learning experience for me.
This is the response for successful login
{"result":{"login":{"isAuthorized":true,"isEmpty":false,"userName":{"isEmpty":false,"name":{"firstName":"Jason","lastName":"Test","displayName":"Test, Jason","isEmpty":false,"fullName":"Jason Test"},"canDelete":false,"id":5793,"canModify":false},"username":"test#testable.com"},"parameters":{"isEmpty":false,"keep_logged_in_indicator":false,"username":"test#testable.com"}},"isAuthorized":true,"version":{"major":"2021","minor":"004","fix":"04","display":"2021.004.04","isEmpty":false},"isSystemDown":false,"timestamp":"2021-07-28T02:47:33Z","isSuccess":true}
This is the response for failure
{"isAuthorized":true,"version":{"major":"2021","minor":"004","fix":"04","display":"2021.004.04","isEmpty":false},"isSystemDown":false,"errors":[{"password":"Unable to login as 'test#testable.com'"}],"timestamp":"2021-07-28T02:47:05Z","isSuccess":false}
This is the code I have written for my api calls
func request<T: Decodable>(endPoint: EndPoint, method: Method, parameters: [String: Any]? = nil, completion: #escaping(Result<T, Error>) -> Void) {
// Creates a urlRequest
guard let request = createRequest(endPoint: endPoint, method: method, parameters: parameters) else {
completion(.failure(AppError.invalidUrl))
return
}
let session = URLSession.shared
session.dataTask(with: request) { data, response, error in
var results: Result<Data, Error>?
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
completion(.failure(AppError.badStatusCode))
return
}
if let response = response {
// Gets the JSESSIONID
let cookieName = "JSESSIONID"
if let cookie = HTTPCookieStorage.shared.cookies?.first(where: { $0.name == cookieName }) {
debugPrint("\(cookieName): \(cookie.value)")
}
print(response)
}
if let data = data {
results = .success(data)
// Converts data to readable String
let responseString = String(data: data, encoding: .utf8) ?? "unable to convert to readable String"
print("Server Response: \(responseString.description)")
} else if let error = error {
results = .failure(error)
print("Server Error: \(error.localizedDescription)")
}
DispatchQueue.main.async {
self.handleResponse(result: results, completion: completion)
}
}.resume()
}
private func handleResponse<T: Decodable>(result: Result<Data, Error>?, completion: (Result<T, Error>) -> Void) {
guard let result = result else {
completion(.failure(AppError.unknownError))
return
}
switch result {
case .success(let data):
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print("Server JsonObject response: \(json)")
} catch {
completion(.failure(AppError.errorDecoding))
}
let decoder = JSONDecoder()
// Decodes that json data
do {
} catch {
}
case .failure(let error):
completion(.failure(error))
}
}
Im mostly interesting in being able to display the json error that occurs when credentials are incorrect. The deadline for my project Is slowing approaching and any help or suggestions would be much appreciated.
You can use Swift's Result type to differentiate a successful result from a failed result.
The Result type is not decodable by default so you will need to write a custom decoder like this:
struct Response: Decodable {
let result: Swift.Result<Result, Errors>
enum CodingKeys: String, CodingKey {
case isSuccess
case errors
case result
}
struct Result: Codable {
let login: Login
struct Login: Codable {
let isAuthorized: Bool
}
}
struct Errors: Error {
let contents: [[String: String]]
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if try container.decode(Bool.self, forKey: .isSuccess) {
result = .success(try container.decode(Result.self, forKey: .result))
} else {
result = .failure(
Errors(contents: try container.decode([[String: String]].self, forKey: .errors))
)
}
}
}

How write parseJSON universal function? Swift

I have different structs as my dataModels.
when i want to parse data with JsonDecoder().decode i need set a .Type.self in .decoder(SomeType.self , from: data)
I want write a support function which can return right Type respectively.
something like this
But I don't know how...
func check<T>(string: String) -> T
if string == "something" {
return Something.type
}
func parseJSON(from data: Data , with address: String)-> Codable? {
let type = check(string: address)
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(type.self, from: data)
return decodedData
} catch let error {
print(error)
return nil
}
}
WHEN I WRITE THE code below, Everything works fine. But I always have to write parseJSON func with little difference over and over again
func fetch(from adress: String) {
guard let url = URL(string: adress) else {print("can't get URL from current urlAdress"); return}
let json = makeHttpBody(from: adress)
let jsonData = try? JSONSerialization.data(withJSONObject: json)
var request = URLRequest(url: url , cachePolicy: .useProtocolCachePolicy)
request.httpMethod = K.post
request.setValue(K.contentType, forHTTPHeaderField:K.applicationJson)
request.timeoutInterval = Double.infinity
request.httpBody = jsonData
let session = URLSession(configuration: .default)
let task = session.dataTask(with: request) { (data, responce, error) in
if error != nil {
print(error!.localizedDescription)
}
if let safeData = data {
if adress == K.balanceUrl {
if let parsedData = self.parseJsonBalance(from: safeData) {
self.delegate?.didUpdateData(with: self, with: parsedData)
}
} else if adress == K.projectsUrl {
if let parsedData = self.parseJsonProject(from: safeData) {
self.delegate?.didUpdateData(with: self, with: parsedData)
}
}
}
}
task.resume()
}
func makeHttpBody(from StringData: String) -> [String: Any] {
switch StringData {
case K.balanceUrl:
return K.authorization
case K.projectsUrl:
return K.projects
default:
return ["none" : "none"]
}
}
Your approach cannot work, you have to use a generic type constrained to Decodable. Checking for strings at runtime is not a good practice.
This is a reduced version of your code, the error is handed over to the caller
func parseJSON<T : Decodable>(from data: Data) throws -> T {
return try JSONDecoder().decode(T.self, from: data)
}
Then you can write (the return type must be annotated)
let result : [Foo] = try parseJSON(from: data)
Or if you want to specify the type in the function
func parseJSON<T : Decodable>(from data: Data, type : T.Type) throws -> T {
return try JSONDecoder().decode(T.self, from: data)
}
let result = try parseJSON(from: data, type: [Foo].self)

Swift Searching For Values inside values of Dictionary

I have a URL which my app fetches. it prints a dictionary with two keys but inside one of the keys is a lot of information I would like to get for my app.
The URL gets lots of information but not as a conventional dictionary.
this is a VERY simplified version:
["person":
name: John
height: 187, "fruit": colour: red
]
etc...
so I would just want to get the name of the person inside the key person but I am having trouble finding this.
Is there any way to do this? I have been trying JSON Parsing, for loops and I am stuck.
Edit:
it isn't a dictionary inside a dictionary. If you would like to see what I am working with. Just copy and paste this link. It is an example of what I am using. http://itunes.apple.com/lookup?bundleId=com.burbn.instagram
I would need just the seller name or just the currency etc.
Code to read the link and print it:
override func viewDidLoad() {
super.viewDidLoad()
fetchData { (dict, error) in
print(dict!)
}
}
func fetchData(completion: #escaping ([String:Any]?, Error?) -> Void) {
let url = URL(string: link)!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let array = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:Any]{
completion(array, nil)
}
} catch {
print(error)
completion(nil, error)
}
}
task.resume()
}
The data you are fetching is JSON. In order to use it, you will have to decode it. The recommended way is using JSONDecoder in Swift.
First you will have to define your model, which correspond to the data model, and make it conform to Codable protocol:
struct App: Codable {
var sellerName: String
// Alternatively, if you don't want to use an enum, you can use a String.
var currency: Currency
enum Currency: String, Codable {
case australianDollar = "AUD",
case britishPound = "GBP",
case euro = "EUR",
case hongKongDollar = "HKD",
case usDollar = "USD"
// Complete this with all the currency…
}
}
struct JSONResult: Codable {
var resultCount: Int
var results: [App]
}
Once this is done, you only have to edit your fetchData method so it returns an array App populated with the data you fetched.
Swift 4 version:
func fetchData(completion: #escaping (JSONResult?, Error?) -> Void) {
guard let url = URL(string: link) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(nil, error)
return
} else if let data = data {
do {
let decoder = JSONDecoder()
let result = try decoder.decode(JSONResult.self, from: data)
completion(result, nil)
} catch {
print(error)
completion(nil, error)
}
}
}
task.resume()
}
Swift 5 version using Result type:
func fetchData(completion: #escaping (Result<JSONResult, Error>) -> Void) {
guard let url = URL(string: link) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
return
} else if let data = data {
do {
let decoder = JSONDecoder()
let result = try decoder.decode(JSONResult.self, from: data)
completion(.success(result))
} catch {
print(error)
completion(.failure(error))
}
}
}
task.resume()
}
More information about JSONDecoder
Dictionary data is:
let dict = ["person": ["name": "John", "height": "187"], "fruit": ["colour": "red"]]
Suppose you need name of the person. So you can do it by the following way.
if let person = dict["person"], let name = person["name"] as? String {
print (name)
}

JSON Parsing in Swift 4

I'm quite new to Swift and i am trying to return a string from my parsed JSON array. I'm not sure how i use the "arrayString" var in another class as i haven't "declared" it in the other class. I assume it has something to do with it being in URLSession. Any tips on what i could do?
struct Games: Decodable {
let videoLink: String
}
class BroadService: NSObject {
static let sharedInstance = BroadService()
func fetchBroadcasts(completion: #escaping ([Games]) -> ()) {
let jsonUrlString = "LINK IS HERE."
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let json = try JSONDecoder().decode([Games].self, from: data)
let arrayString = (json[linkcell].videoLink)
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
This is my code at the minute, It's not the most neat thing in the world, but it gets the job done.
There are several issues with your code:
You needlessly extend NSObject.
You never call the completion block.
arrayString is not an array.
Your completion block should at least make the Games array optional to indicate an error.
Here's how your code should be:
struct Games: Decodable {
let videoLink: String
}
class BroadService {
static let sharedInstance = BroadService()
func fetchBroadcasts(completion: #escaping ([Games]?) -> ()) {
let jsonUrlString = "LINK IS HERE."
guard let url = URL(string: jsonUrlString) else {
completion(nil)
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completion(nil)
return
}
do {
let games = try JSONDecoder().decode([Games].self, from: data)
completion(games)
} catch let jsonErr {
print("Error deserializing json:", jsonErr)
completion(nil)
}
}.resume()
}
}
Then you can call it as follows:
BroadService.sharedInstance.fetchBroadcasts { (games) in
if let games = games {
// Process all games if needed
for game in games {
let videoLink = game.videoLink
// do something
}
// Or access a specific game:
let game = games[someIndex]
let videoLink = game.videoLink
// do something
}
}

Swift passing method params Struct Decodable

,Swift 4 how can I pass Decodable Struct in method params and parse it in JSONDecoder()?
error:
Cannot invoke 'decode' with an argument list of type '(Decodable,
from: Data)'
struct JsonRespons: Codable {
let uid: String
let msisdn: String
let APK: String
let fname: String
let lname: String
}
struct JsonResponsError: Decodable {
let uid: String
let error: String
}
extension UIView {
func phoneAuth(serverApi path:String, jsonStruct:Codable){
let jsonUrlString = Globals.JOSN_API_URL + path
guard let url = URL(string: jsonUrlString) else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard err == nil else {
return
}
guard let data = data else { return }
do {
let result = try JSONDecoder().decode(jsonStruct.self, from: data)
self.handleJsonResult(resalt: result as AnyObject)
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
func handleJsonResult(resalt:AnyObject){
print(resalt)
}
}
Adding Codable to the inheritance list for Landmark triggers an automatic conformance that satisfies all of the protocol requirements from Encodable and Decodable:
You can Use Codable
struct Landmark: Codable {
var name: String
var foundingYear: Int
// Landmark now supports the Codable methods init(from:) and encode(to:),
// even though they aren't written as part of its declaration.
}
Alternative solution is
func phoneAuth(serverApi path: String, Completion block: #escaping ((Data) -> ())) {
URLSession.shared.dataTask(with: URL(string: url)!) { (data, res, err) in
if let d = data {
block(d)
}
}.resume()
}
Call a Methods
phoneAuth(serverApi: "yourUrl") { (data) in
do {
let result = try JSONDecoder().decode(YourDecodable.self, from: data)
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}
You don't have to pass it as a parameter, you can achieve decoding like below
extension UIView {
func phoneAuth(serverApi path:String){
let jsonUrlString = Globals.JOSN_API_URL + path
guard let url = URL(string: jsonUrlString) else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard err == nil else {
return
}
guard let data = data else { return }
do {
let result = try JSONDecoder().decode(JsonRespons.self, from: data)
self.handleJsonResult(resalt: result as AnyObject)
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}