Decoding JSON in Swift 4 - json

I am working through the Apple App Development Guide and this is the code I am working with right now...
struct CategoryInfo: Codable {
var category: String
var description: String
var logo: String
var mobileCategoryName: String
enum Keys: String, CodingKey {
case category
case description = "descr"
case logo
case mobileCategoryName = "mobileCatName"
}
init(from decoder: Decoder) throws {
let valueContainer = try decoder.container(keyedBy: Keys.self)
self.category = try valueContainer.decode(String.self, forKey: Keys.category)
self.description = try valueContainer.decode(String.self, forKey: Keys.description)
self.logo = try valueContainer.decode(String.self, forKey: Keys.logo)
self.mobileCategoryName = try valueContainer.decode(String.self, forKey: Keys.mobileCategoryName)
}
}
override func viewDidLoad() {
super.viewDidLoad()
let categories = Industry_TableViewController()
categories.fetchCategoryInfo { (category) in
if let category = category {
print(category)
}
}
}
func fetchCategoryInfo(completion: #escaping(CategoryInfo?) -> Void) {
let url = URL(string: "XXXXX")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let category = try? jsonDecoder.decode(CategoryInfo.self, from: data) {
completion(category)
} else {
print("Nothing reutrned or Not decoded")
completion(nil)
}
}
task.resume()
}
it works fine when my returned JSON is in the following format...
{"category":"Excavators","descr":"Compact, Mid-Sized, Large, Wheeled, Tracked...","logo":"excavators","mobileCatName":"Excavators"}
My struct is created and all the variables are populated correctly. But the API doesn't bring back one category at a time it brings back multiple like so...
[{"category":"Aerial Lifts","descr":"Aerial Lifts, Man Lifts, Scissor Lifts...","logo":"aeriallifts","mobileCatName":"Aerial Lifts"},{"category":"Aggregate Equipment","descr":"Crushing, Screening, Conveyors, Feeders and Stackers...","logo":"aggregateequipment","mobileCatName":"Aggregate"},{"category":"Agricultural Equipment","descr":"Tractors, Harvesters, Combines, Tillers...","logo":"agricultural","mobileCatName":"Agricultural"}]
And I am running into a wall trying to figure out how to get this decoded properly. I've gone down so many routes I don't even know what to search for any more. Can anyone help or point me in a direction.

You need to modify your function to parse an array of categories instead of a single one. You just need to pass the Array<CategoryInfo> metatype to the decode function and modify the function signature such that the completion handler also returns an array.
func fetchCategoryInfo(completion: #escaping ([CategoryInfo]?) -> Void) {
let url = URL(string: "XXXXX")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let categories = try? jsonDecoder.decode([CategoryInfo].self, from: data) {
completion(categories)
} else {
print("Nothing reutrned or Not decoded")
completion(nil)
}
}
task.resume()
}

try? jsonDecoder.decode([CategoryInfo].self, from: data)

Related

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)

Could not cast value of type 'NSNull' to 'NSString' and then the app crashes

We are trying to make a function to get JSON from an API..We know that this is giving us NIL but we dont know why the error is occuring. The exact error message that we got was
[]
2020-08-01 16:29:26.501199-0400 HEFT[97766:2952325] [] nw_proxy_resolver_create_parsed_array [C1 proxy pac] Evaluation error: NSURLErrorDomain: -1003
Could not cast value of type 'NSNull' (0x7fff87a92380) to 'NSString' (0x7fff87b502e8).
2020-08-01 16:29:26.670549-0400 HEFT[97766:2952139] Could not cast value of type 'NSNull' (0x7fff87a92380) to 'NSString' (0x7fff87b502e8).
(lldb)
We have tried messing around the code to find a solution and we tried to use some other questions but none of them were related with what we were trying to achieve.
func getJson() {
if let url = URL(string: "https://api.weather.gov/alerts/active?area=GA") {
URLSession.shared.dataTask(with: url) { (data:Data?, response:URLResponse?, error:Error?) in
if error == nil {
if data != nil {
if let json = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String:AnyObject] {
DispatchQueue.main.async {
//if let rawfeatures = json["features"] {
var rawfeatures = json["features"] as! [Dictionary< String, AnyObject>]
var keepgoingfeatures = rawfeatures.count
var FeatureIndex = 0
while keepgoingfeatures != 0{
let currentRawFeature = rawfeatures[FeatureIndex]
let currentRawFeatureProperties = currentRawFeature["properties"]
let currentFeature = Feature()
currentFeature.event = currentRawFeatureProperties!["event"] as! String
currentFeature.description = currentRawFeatureProperties!["description"] as! String
currentFeature.instructions = currentRawFeatureProperties!["instruction"] as! String
currentFeature.urgency = currentRawFeatureProperties!["urgency"] as! String
keepgoingfeatures -= 1
FeatureIndex += 1
}
}
}
}
} else {
print("We have an error")
}
}.resume()
}
}
Some of these alerts have null for instructions. I’d suggest defining your object to acknowledge that this field is optional, i.e. that it might not be present. E.g.
struct Feature {
let event: String
let description: String
let instruction: String?
let urgency: String
}
And, when parsing it, I might suggest getting rid of all of those forced unwrapping operators, e.g.
enum NetworkError: Error {
case unknownError(Data?, URLResponse?)
case invalidURL
}
#discardableResult
func getWeather(area: String, completion: #escaping (Result<[Feature], Error>) -> Void) -> URLSessionTask? {
// prepare request
var components = URLComponents(string: "https://api.weather.gov/alerts/active")!
components.queryItems = [URLQueryItem(name: "area", value: area)]
var request = URLRequest(url: components.url!)
request.setValue("(\(domain), \(email))", forHTTPHeaderField: "User-Agent")
// perform request
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
let responseData = data,
let responseDictionary = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any],
let rawFeatures = responseDictionary["features"] as? [[String: Any]]
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknownError(data, response)))
}
return
}
let features = rawFeatures.compactMap { feature -> Feature? in
guard
let properties = feature["properties"] as? [String: Any],
let event = properties["event"] as? String,
let description = properties["description"] as? String,
let urgency = properties["urgency"] as? String
else {
print("required string absent!")
return nil
}
let instruction = properties["instruction"] as? String
return Feature(event: event, description: description, instruction: instruction, urgency: urgency)
}
DispatchQueue.main.async {
completion(.success(features))
}
}
task.resume()
return task
}
A few other observations:
I’ve removed all of the forced casting (the as!). You don’t want your app crashing if there was some problem in the server. For example, not infrequently I receive a 503 error. You don’t want to crash if the server is temporarily unavailable.
The docs say that you should set the User-Agent, so I’m doing that above. Obviously, set the domain and email string constants accordingly.
While you can build the URL manually, it’s safest to use URLComponents, as that will take care of any percent escaping that might be needed. It’s not needed here, but will be a useful pattern if you start to get into more complicated requests (e.g. need to specify a city name that has a space in it, such as “Los Angeles”).
I’d suggest the above completion handler pattern so that the caller can know when the request is done. So you might do something like:
getWeather(area: "GA") { result in
switch result {
case .failure(let error):
print(error)
// update UI accordingly
case .success(let features):
self.features = features // update your model object
self.tableView.reloadData() // update your UI (e.g. I'm assuming a table view, but do whatever is appropriate for your app
}
}
I’m returning the URLSessionTask in case you might want to cancel the request (e.g. the user dismisses the view in question), but I’ve marked it as a #discardableResult, so you don’t have to use that if you don’t want.
I’ve replaced the tower of if statements with a guard statement. It makes the code a little easier to follow and adopts an “early exit” pattern, where you can more easily tie the exit code with the failure (if any).
Personally, I’d suggest that you take this a step further and get out of manually parsing JSONSerialization results. It’s much easier to let JSONDecoder do all of that for you. For example:
struct ResponseObject: Decodable {
let features: [Feature]
}
struct Feature: Decodable {
let properties: FeatureProperties
}
struct FeatureProperties: Decodable {
let event: String?
let description: String
let instruction: String?
let urgency: String
}
enum NetworkError: Error {
case unknownError(Data?, URLResponse?)
case invalidURL
}
#discardableResult
func getWeather(area: String, completion: #escaping (Result<[FeatureProperties], Error>) -> Void) -> URLSessionTask? {
var components = URLComponents(string: "https://api.weather.gov/alerts/active")!
components.queryItems = [URLQueryItem(name: "area", value: area)]
var request = URLRequest(url: components.url!)
request.setValue("(\(domain), \(email))", forHTTPHeaderField: "User-Agent")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
let responseData = data
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknownError(data, response)))
}
return
}
do {
let responseObject = try JSONDecoder().decode(ResponseObject.self, from: responseData)
DispatchQueue.main.async {
completion(.success(responseObject.features.map { $0.properties }))
}
} catch let parseError {
DispatchQueue.main.async {
completion(.failure(parseError))
}
}
}
task.resume()
return task
}
The short answer is because you force cast everything and assume a very specific format which the json doesnt have.
so at some point you read a value that just insnt there.
Concretely instruction.
as a working/non crashing fix (which I locally ran!):
let currentFeature = Feature()
currentFeature.event = currentRawFeatureProperties!["event"] as? String ?? ""
currentFeature.description = currentRawFeatureProperties!["description"] as? String ?? ""
currentFeature.instructions = currentRawFeatureProperties!["instruction"] as? String ?? ""
currentFeature.urgency = currentRawFeatureProperties!["urgency"] as? String ?? ""
I'd urge you to refactor your function broadly

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

Writing a JSON serialization function

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

URLSession with URL returns Error

I have read topics on SO but not found an answer yet.
I have been trying to use a weather api to download weather data for my app. Strangely I can run it on urls without a '?' but this url has a '?' built in. I suspect this is the problem but how do I fix it, or get it to ignore it? That is my theory anyhow. Heres the code:
struct WeatherData: Decodable {
let description: String
let temp: Double
let wind: Double
}
func weather() {
let url = "http://api.openweathermap.org/data/2.5/weather?q=London,GB?&units=imperial&APPID={40b5f59a0004885043fe3df3e0b6ed8e}"
let urlObj = URL(string: url)
URLSession.shared.dataTask(with: urlObj!) {(data, response, error) in
do {
let weatherObj = try JSONDecoder().decode([WeatherData].self, from: data!)
print(data!)
for weather in weatherObj {
print(weather.temp, weather.description, weather.wind)
}
} catch {
print("Got an Error")
}
}.resume()
}
So running that as is produces the error: "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value" or the URLSession line.
Am I missing something really obvious or is there a way to fix this?
Many thanks
--
Updated:
So after changing the structs and {} it was working until I began to enter data into labels. Heres the latest attempt:
func weather() {
let lat = locationManager.location!.coordinate.latitude
let long = locationManager.location!.coordinate.longitude
//let baseURL = "http://api.openweathermap.org/data/2.5/weather?"
let apiKey = "40b5f59a0004885043fe3df3e0b6ed8e"
//let weatherURL = URL(string: "\(baseURL)lat=\(lat)&lon=\(long)&units=metric&APPID=\(apiKey)")
let weahterURL = "http://api.openweathermap.org/data/2.5/weather?lat=\(lat)&lon=\(long)&units=metric&APPID=\(apiKey)"
//let url = "http://api.openweathermap.org/data/2.5/weather?q=London,GB?&units=imperial&APPID=40b5f59a0004885043fe3df3e0b6ed8e"
let urlObj = URL(string: weahterURL)
URLSession.shared.dataTask(with: urlObj!) {(data, response, error) in
do {
let weatherObj = try JSONDecoder().decode(WeatherData.self, from: data!)
print(weatherObj)
//seems as though not gettign any data from beyond this point
var desc = weatherObj.weather
var wind = weatherObj.wind.speed
var tempMin = weatherObj.main.temp_min
var tempMax = weatherObj.main.temp_max
DispatchQueue.main.async {
self.weatherDesc = desc
self.weartherWind.text = wind
self.tempMinTxt.text = tempMin
self.tempMaxTxt.text = tempMax
}
} catch {
print("Got an Error", error.localizedDescription)
}
}.resume()
}
You mistakenly construct the url instead of
let url = "http://api.openweathermap.org/data/2.5/weather?q=London,GB?&units=imperial&APPID={40b5f59a0004885043fe3df3e0b6ed8e}"
do
let url = "http://api.openweathermap.org/data/2.5/weather?q=London,GB?&units=imperial&APPID=40b5f59a0004885043fe3df3e0b6ed8e"
This
{40b5f59a0004885043fe3df3e0b6ed8e}
should be
40b5f59a0004885043fe3df3e0b6ed8e
Also the struct you create for the decoder is not valid and won't get the data
//
struct WeatherData: Decodable {
let weather: [WeatherItem]
let wind: WindItem
let main : MainItem
}
//
struct WeatherItem: Decodable {
let description: String
}
//
struct WindItem: Decodable {
let speed: Double
let deg: Double
}
//
struct MainItem : Decodable {
let tempMin: Double
let tempMax: Double
private enum CodingKeys: String, CodingKey {
case tempMin = "temp_min" , tempMax = "temp_max"
}
}
//
let weatherObj = try JSONDecoder().decode(WeatherData.self, from: data!)