I parse JSON in my application and some of the JSONs have nil values which I handled. However, the app still shows me the error that JSON contains nil values. My code:
struct Response : Decodable {
let articles: [Article]
}
struct Article: Decodable {
let title: String
let description : String
let url : String
let urlToImage : String
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let article = try JSONDecoder().decode(Response.self , from : data)
for i in 0...article.articles.count - 1 {
if type(of: article.articles[i].title) == NSNull.self {
beforeLoadNewsViewController.titleArray.append("")
} else {
beforeLoadNewsViewController.titleArray.append(article.articles[i].title)
}
if type(of : article.articles[i].urlToImage) == NSNull.self {
beforeLoadNewsViewController.newsImages.append(newsListViewController.newsImages[newsListViewController.newsIndex])
} else {
let url = URL(string: article.articles[i].urlToImage ?? "https://static.wixstatic.com/media/b77fe464cfc445da9003a5383a3e1acf.jpg")
let data = try? Data(contentsOf: url!)
if url != nil {
//make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
let img = UIImage(data: data!)
beforeLoadNewsViewController.newsImages.append(img!)
} else {
beforeLoadNewsViewController.newsImages.append(newsListViewController.newsImages[newsListViewController.newsIndex])
}
This is the error running the app:
FC91DEECC1631350EFA71C9C561D).description], debugDescription: "Expected String value but found null instead.", underlyingError: nil))
Note:
This JSON works with other urls that don't have nil values.
and here is the json
{
"status": "ok",
"source": "entertainment-weekly",
"sortBy": "top",
"articles": [
{
"author": null,
"title": "Hollywood's Original Gone Girl",
"description": null,
"url": "http://mariemcdonald.ew.com/",
"urlToImage": null,
"publishedAt": null
},
{
"author": "Samantha Highfill",
"title": "‘Supernatural’: Jensen Ackles, Jared Padalecki say the boys aren’t going ‘full-mope’",
"description": "",
"url": "http://ew.com/tv/2017/10/18/supernatural-jensen-ackles-jared-padalecki-season-13/",
"urlToImage": "http://ewedit.files.wordpress.com/2017/10/supernatural-season-13-episode-1.jpg?crop=0px%2C0px%2C2700px%2C1417.5px&resize=1200%2C630",
"publishedAt": "2017-10-18T17:23:54Z"
},
{
The error is quite clear. JSONDecoder maps NSNull to nil so the decoder throws an error if it's going to decode a nil value to a non-optional type.
The solution is to declare all affected properties as optional.
let title: String
let description : String?
let url : String
let urlToImage : String?
or customize the decoder to replace nil with an empty string.
And because JSONDecoder maps NSNull to nil the check if ... == NSNull.self { is useless.
Edit:
Don't use ugly C-style index based loops use
let response = try JSONDecoder().decode(Response.self , from : data)
for article in response.articles {
beforeLoadNewsViewController.titleArray.append(article.title)
}
PS: But why for heaven's sake do you map the article instance to – apparently – separate arrays? You got the Article instances which contain everything related to one article respectively.
I am sorry, I cannot comment yet. But why don't you test for "null" the way, you did at the URLSession data check, with the guard-statement?
It would then look like:
guard let title = article.articles[i].title else {
beforeLoadNewsViewController.titleArray.append("")
return
}
beforeLoadNewsViewController.titleArray.append(title)
You try to parse a JSON structure here, in case the JSON structure would not fit to your needs, you still expect it to have the according attributes.
Could you also provide your JSON structure and how your decoding
try JSONDecoder().decode(Response.self , from : data)
looks like?
Related
I am trying to get the title from json data but I am getting an error. Here is my model object
struct SpaceNewsModel: Identifiable, Codable {
var id = UUID()
let title: String
let url: String
let source: String
}
And when I get to this point in my networking, I get an error.
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
if error != nil {
print(error!)
} else {
let decoder = JSONDecoder()
if let safeData = data {
do {
let astroNews = try decoder.decode([SpaceNewsModel].self, from: safeData)
print(astroNews[0].title)
} catch {
print("DEBUG: Error getting news articles \(error.localizedDescription)")
}
}
let httpResponse = response as? HTTPURLResponse
// print(httpResponse)
}
})
task.resume()
}
My main problem is with the line print(astroNews[0].title I am not formatting it right and I don't know how to access this data whether in an array or otherwise.
Here is the returned JSON data after making the request on the website postman.
[
{
"title": "Wales' new £2bn space strategy hopes",
"url": "https://www.bbc.co.uk/news/uk-wales-60433763",
"source": "bbc"
},
{
"title": "Could Port Talbot become a centre of space tech? Video, 00:02:02Could Port Talbot become a centre of space tech?",
"url": "https://www.bbc.co.uk/news/uk-wales-60471170",
"source": "bbc"
}
]
Normally there would be a title for each that I can access but here it is just the data. So to summarize, how would I get the first or second title in this JSON data? Because astroNews[0].title does not work.
EDIT: I want to clarify that if I were to decode an empty struct, I would not get any errors.
struct EMPTYSTRUCT: Codable {
}
But as soon as I add any variable like let title: String I get an error. I believe something is wrong in the formatting of the JSON because usually the JSON would look something this
{
"coord": {
"lon": -95.3633,
"lat": 29.7633
}
}
where I could name the struct "coord" however no such names exist in the returned JSON I'm working with. Only the variable names. The confusion is how would I construct my SpaceNewModel file to work with the returned JSON with no apparent object name.
With reference to this answer.
Just change your structure to
struct SpaceNewsModel: Identifiable, Codable {
let id = UUID()
let title: String
let url: String
let source: String
}
This is not the solution but a workaround; if you want to solve the issue, refer to this answer
I'm pretty sure my model is correct based on my data, I cannot figure out why I am getting the format error?
JSON:
{
"1596193200":{
"clientref":1,
"type":"breakfast"
},
"1596200400":{
"clientref":0,
"type":"lunch"
},
"1596218400":{
"clientref":2,
"type":"dinner"
}
}
model:
struct Call: Decodable {
let clientref: Int?
let type: String?
}
edit updated question with the code for decoding the json data from the URL:
class CallService {
static let shared = CallService()
let CALLS_URL = "url.com/Calls.json"
func fetchCalls(completion: #escaping ([Call]) -> ()) {
guard let url = URL(string: CALLS_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
print("Failed to fetch data with error: ", error.localizedDescription)
return
}
guard let data = data else {return}
do {
let call = try JSONDecoder().decode([Call].self, from: data)
completion(call)
} catch let error {
print("Failed to create JSON with error: ", error.localizedDescription)
}
}.resume()
}
}
I strongly suggest to learn how to debug: it includes where to look, what info to get, where to get them, etc, and at the end, fix it.
That's a good thing that you print the error, most beginner don't.
print("Failed to create JSON with error: ", error.localizedDescription)
=>
print("Failed to create JSON with error: ", error)
You'll get a better idea.
Second, if it failed, print the data stringified. You're supposed to have JSON, that's right. But how often do I see question about that issue, when it fact, the answer wasn't JSON at all (the API never stated it will return JSON), the author were facing an error (custom 404, etc.) and did get a XML/HTML message error etc.
So, when the parsing fails, I suggest to do:
print("Failed with data: \(String(data: data, encoding: .utf8))")
Check that the output is a valid JSON (plenty of online validators or apps that do that).
Now:
I'm pretty sure my model is correct based on my data,
Well, yes and no.
Little tip with Codable when debuting (and not using nested stuff): Do the reverse.
Make your struct Codable if it's not the case yet (I used Playgrounds)
struct Call: Codable {
let clientref: Int?
let type: String?
}
do {
let calls: [Call] = [Call(clientref: 1, type: "breakfast"),
Call(clientref: 0, type: "lunch"),
Call(clientref: 2, type: "dinner")]
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
let jsonData = try encoder.encode(calls)
let jsonStringified = String(data: jsonData, encoding: .utf8)
if let string = jsonStringified {
print(string)
}
} catch {
print("Got error: \(error)")
}
Output:
[
{
"clientref" : 1,
"type" : "breakfast"
},
{
"clientref" : 0,
"type" : "lunch"
},
{
"clientref" : 2,
"type" : "dinner"
}
]
It doesn't look like. I could only used an array to put various calls inside a single variable, and that's what you meant for decoding, because you wrote [Call].self, so you were expecting an array of Call. We are missing the "1596218400" parts. Wait, could it be a dictionary at top level? Yes. You can see the {} and the fact it uses "keys", not listing one after the others...
Wait, but now that we printed the full error, does it make more sense now?
typeMismatch(Swift.Array<Any>,
Swift.DecodingError.Context(codingPath: [],
debugDescription: "Expected to decode Array<Any> but found a dictionary instead.",
underlyingError: nil))
Fix:
let dictionary = try JSONDecoder().decode([String: Call].self, from: data)
completion(dictionary.values) //since I guess you only want the Call objects, not the keys with the numbers.
From the code you provided it looks like you are trying to decode an Array<Call>, but in the JSON the data is formatted as a Dictionary<String: Call>.
You should try:
let call = try JsonDecoder().decode(Dictionary<String: Call>.self, from: data)
I have json response that I need to parse that, according to the https://newsapi.org web site, should look like:
{
"status": "ok",
"totalResults": 4498,
"articles": [{
"source": {
"id": null,
"name": "Techweekeurope.co.uk"
},
"author": null,
"title": "Top ten cloud service providers for reliability and price",
"description": "In a time where the reliability and stability of cloud service providers comes into spotlight, we pick our top ten that will help you propel your business to the next level. We recently talked about building your own local network with a Raspberry Pi starter …",
"url": "https://www.techweekeurope.co.uk/top-ten-cloud-service-providers-reliability-price/",
"urlToImage": "https://www.techweekeurope.co.uk/wp-content/uploads/2020/01/Cloud-Service-Providers.gif",
"publishedAt": "2020-01-04T16:17:00Z",
"content": "In a time where the reliability and stability of cloud service providers comes into spotlight, we pick our top ten that will help you propel your business to the next level. We recently talked about building your own local network with a Raspberry Pi starter … [+4441 chars]"
}, ...]
}
I created structures for parse it
import Foundation
struct Article: Codable {
var source: Source
var author: String?
var title: String
var description: String
var url: URL
var urlToImage: URL?
var content: String
enum codingKeys: String, CodingKey {
case source
case author
case title
case description
case url
case urlToImage
case content
}
}
struct Source: Codable {
var name: String
}
struct Articles: Codable {
var articles: [Article]
}
And I created class network service class
class NetworkService {
// MARK: - Methods
func parseJSON(from url: URL) {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
let decoder = JSONDecoder()
let articleData = try decoder.decode(Articles.self, from: data!)
print(articleData.articles.description)
} catch {
print(error)
}
}
task.resume()
}
}
In console I have this:
keyNotFound(CodingKeys(stringValue: "articles", intValue: nil),
Swift.DecodingError.Context(codingPath: [], debugDescription: "No
value associated with key CodingKeys(stringValue: \"articles\",
intValue: nil) (\"articles\").", underlyingError: nil))
Your error is telling you that the JSON was valid, but that it was unable to find any articles key in the response.
I’d suggest including status and message in your definition of Articles and make articles property an optional. The status might not be "ok". Regardless, articles is obviously absent.
struct Articles: Codable {
let articles: [Article]?
let status: String
let message: String?
let code: String?
}
For example, if you don’t supply a valid key, you’ll get a response like
{
"status": "error",
"code": "apiKeyInvalid",
"message": "Your API key is invalid or incorrect. Check your key, or go to https://newsapi.org to create a free API key."
}
You may want to handle the case where articles may be absent if there is an error, by making it optional.
Unrelated, but the forced unwrapping operator for data is dangerous. If you have some network error, your app will crash. I’d suggest unwrapping it, e.g.:
func parseJSON(from url: URL) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
do {
let articleData = try JSONDecoder().decode(Articles.self, from: data)
guard let articles = articleData.articles else {
print("No articles", articleData.status, articleData.message ?? "No message")
return
}
for article in articles {
print(article.description)
}
} catch {
print(error)
}
}
task.resume()
}
This is JSON what I want to parse and use in my app
{
"status": "ok",
"source": "the-next-web",
"sortBy": "latest",
-"articles": [
-{
"author": "Mix",
"title": "Social media giants like Facebook liable to €50M ‘hate speech’ fines in Germany",
"description": "Germany will soon punish social media giants like Facebook, Twitter and YouTube with fines of up to €50 million (approximately $57 million) if they fail to take down illegal or offensive ...",
"url": "https://thenextweb.com/facebook/2017/06/30/facebook-youtube-twitter-germany-50m/",
"urlToImage": "https://cdn0.tnwcdn.com/wp-content/blogs.dir/1/files/2017/03/facebook.jpg",
"publishedAt": "2017-06-30T15:10:58Z"
},
-{
"author": "Abhimanyu Ghoshal",
"title": "Google’s new Android app makes it easy to save mobile data on the go",
"description": "The good folks at Android Police have spotted a new app from Google called Triangle; it lets you control which other apps can use your mobile data. It's a handy little tool if you're ...",
"url": "https://thenextweb.com/apps/2017/06/30/googles-new-android-app-makes-it-easy-to-save-mobile-data-on-the-go/",
"urlToImage": "https://cdn0.tnwcdn.com/wp-content/blogs.dir/1/files/2017/06/Triangle-hed.jpg",
"publishedAt": "2017-06-30T13:16:12Z"
},
-{
This is my methods to parse the Json:
typealias GetWeatherSuccess = (_ result: NSDictionary) -> Void
typealias GetWeatherFailure = (_ error: Error?) -> Void
func requestNewsForToday() -> URLRequest {
let urlText = NewsManager.shared.weatherRequest()
return URLRequest(url: URL(string: urlText)! as URL)
}
func getNews(successHandler: GetWeatherSuccess?,
failureHandler: GetWeatherFailure?) {
let session = URLSession.shared
let dataTask = session.dataTask(with: requestNewsForToday()) { data, _, error in
if error == nil {
if let dic = try? JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary {
successHandler?(dic!)
}
} else {
failureHandler?(error)
}
}
dataTask.resume()
}
And usage of methods:
NetworkManager.shared.getNews(successHandler: { (json: NSDictionary) in
let articles = json.object(forKey: "articles") as! NSArray
print(articles[0])
}) { (_:Error?) in
print("error")
}
}
And result is oblivious:
{
author = "Romain Dillet";
description = "\U201cWe called this building Station F, like Station France, Station Femmes [ed. note: women in French], Station Founders or Station Freyssinet because..";
publishedAt = "2017-07-01T09:47:38Z";
title = "A walk around Station F with Emmanuel\U00a0Macron";
url = "https://techcrunch.com/2017/07/01/a-walk-around-station-f-with-emmanuel-macron/";
urlToImage = "https://tctechcrunch2011.files.wordpress.com/2017/06/station-f-emmanuel-macron-9.jpg?w=764&h=400&crop=1";
}
How to take authors String for example?
Or make full array of data from json and use it?
I am going to use Swift 4 Decodable protocol to parse JSON the simple and easy way! It's really a very powerful tool that Apple has given in this update!
By looking at the JSON, I can infer I need two structs which will conform to the Decodable protocol:-
struct jsonData: Decodable {
let status: String
let source: String
let sortBy: String
let articles: [Articles]
}
struct Articles: Decodable {
let author: String
let title: String
let description: String
let url: String
let urlToImage: String
let publishedAt: String
}
Pay attention that I have exactly named the variables same as your keys in the JSON. This is where the Decodable protocol magic happens. It automatically infers which key value/pair belongs to which variable on the basis of your variable name (meaning your key name in JSON, should be the variable name in swift), provided you named them correctly, otherwise it will throw an error.
Now, making a network connection as usual:-
URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
do {
let item = try JSONDecoder().decode(jsonData.self, from: data)
print(item.status) // prints "ok"
print(items.articles) // prints the 2 arrays in the json, provided in your Question
}
catch let jsonErr {
print("serialisation error", jsonErr)
}
}
I trying to decode an json object from a webrequest with Swift 3, Gloss 1.1 and Alamofire 4.0:
The json response looks like this:
{
"code": "0",
"message": "OK.",
"data": [
{
"timestamp": 1480885860,
"open": 10.99
},
{
"timestamp": 1480886040,
"open": 11
}
]
}
My json Decodables are the following two:
struct ResponseJsonModel : Decodable {
let code : Int
let message : String
let data : [MarketPriceJsonModel]?
// <~~
init?(json: JSON) {
guard let codeInt : Int = "code" <~~ json else {
print("code unwrapping failed in guard")
return nil
}
guard let messageStr : String = "message" <~~ json else {
print("message unwrapping failed in guard")
return nil
}
self.code = codeInt
self.message = messageStr
self.data = "data" <~~ json
}
}
struct MarketPriceJsonModel : Decodable {
let timestamp : NSDate
let open : Double
init?(json: JSON) {
guard let timestampInt : Int = "timestamp" <~~ json else {
print("timestamp unwrapping failed in guard")
return nil
}
guard let open : Double = "open" <~~ json else {
print("open price unwrapping failed in guard")
return nil
}
self.timestamp = NSDate(timeIntervalSince1970: Double(timestampInt))
self.open = open
}
}
I'm new to Gloss and don't understand why the initalize of my decodable-Object fails.
Alamofire.request(url).validate().responseJSON { response in
switch response.result {
case .success:
guard let value = response.result.value as? JSON,
let responseModel = ResponseJsonModel(json: value) else {
print("responseModel failed")
return
}
print(responseModel.message)
case .failure(let error):
print(error)
}
}
The output of the code is
code unwrapping failed in guard
responseModel failed
but why?
When I add a breakpoint in init?() and look at the json-variable in the debug area, so the request looks ok but the parsing fails.
Is there a way to get better exception messages in case something fails?
Any input appreciated.
Ok, problem solved. It was a faulty configuration of the webservice.
As you can see, the json response of the code attribute was:
"code": "0",
This format clearly stands for a string, therefor my guard with parsing into Int will fail.
To solve this, there are two possible ways I found.
Option 1: either change json response to:
"code": 0,
(value has no more surrounding quotes). This is probably the best solution as it fixes the wrong datatype of the webservice, but it requires to have full control over the codebase of the service.
Option 2: simply parse the json response into a String and after that, force-unwrap into an Int workes too. For this solution the webservice will remain unchanged.
guard let codeStr : String = "code" <~~ json else {
print("code unwrapping failed in guard")
return nil
}
self.code = Int(codeStr)!