How to parse JSON data properly if some data is NSNull - ios

I'm currently pulling data via Twitter's Search API.
Once I receive the data, I am trying to parse and save the data to my TweetDetails class (which is to hold the author's name, the url of their profile pic, the text of the tweet, the location of the tweet, the tweet id and user id).
In some cases, the Tweets do not have a location (I think it has something to do if they're retweeted), and the certain dictionary (here being tweetsDict["place"]) that would otherwise hold that information, instead returns NSNull.
On those occasions, I receive this error
Could not cast value of type 'NSNull' (0x1093a1600) to 'NSDictionary' (0x1093a0fe8).
Here is my code as I am trying to parse the data and save it to objects in my TweetDetails class
client.sendTwitterRequest(request) { (response, data, connectionError) -> Void in
if connectionError != nil {
print("Error: \(connectionError)")
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
if let tweets = json["statuses"] as? [NSDictionary] {
for tweetsDict in tweets {
let text = tweetsDict["text"] as! String
let tweetID = tweetsDict["id_str"] as! String
let place = tweetsDict["place"] as! NSDictionary
// this line ^^ is where the error occurs
let city = place.valueForKey("full_name")
let user = tweetsDict["user"] as! NSDictionary
let userID = user.valueForKey("id_str")
let screenName = user.valueForKey("screen_name")!
let avatarImage = user.valueForKey("profile_image_url_https")!
let tweet = TweetDetails(authors: screenName as! String, profileImages: avatarImage as! String, tweetTexts: text, tweetLocations: city as! String , tweetIDs: tweetID , userIDs: userID as! String)
self.allTweets.append(tweet)
self.tweetTableView.reloadData()
}
}
} catch let jsonError as NSError {
print("json error: \(jsonError.localizedDescription)")
}
}
I have tried to create some 'if-let' statements to provide alternatives for these occasions, but nothing has worked.
Could someone please help me to create a custom alternative around when certain JSON data returns as NSNull. (Even something as simple as turning my "city" variable into the string "Unknown Location" in the cases when the data returns NSNull).
Thanks in advance and please let me know if there is anything else I can add to provide more clarity to this question.

As others have pointed out, you can use optional binding (if let), or, in this case, even easier, just employ optional chaining:
let city = tweetsDict["place"]?["full_name"] as? String
This way, city is an optional, i.e. if no value was found because there was no place or full_name entry, it will be nil.
It would appear, though, that your TweetDetails is force casting city to a String. Does it absolutely require a city? It would be best to change this method so that city was optional and gracefully handle nil values there. Alternatively, you can replace nil values for city to some other string, e.g. with the nil coalescing operator:
let city = tweetsDict["place"]?["full_name"] as? String ?? "Unknown city"
That returns the city if found, and "Unknown city" if not.

I see a lot of force data type casting in your code witch is very dangerous, especially when parsing JSON. The correct way of dealing with nullability is not to avoid it by trying to do the same thing we did in objective-c but to prepare to receive nil and react accordingly. Your data model should represent the fact that sometime a tweet have no location so some properties of TweetDetails should be marked as optionals.
The code exemple bellow is completely untested but give you an idea of what your code can look like dealing with nullability.
client.sendTwitterRequest(request) { (response, data, connectionError) -> Void in
if connectionError != nil {
print("Error: \(connectionError)")
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
if let tweets = json["statuses"] as? [[String: AnyObject]] {
for tweetsDict in tweets {
if let text = tweetsDict["text"] as? String,
tweetID = tweetsDict["id_str"] as? String {
var city: String?
if let place = tweetsDict["place"] as? [String: AnyObject] {
city = place.valueForKey("full_name")
}
var userID: String?
var screenName: String?
var avatarImage: String?
if let user = tweetsDict["user"] as? [String: AnyObject] {
userID = user["id_str"] as? String
screenName = user["screen_name"] as? String
avatarImage = user["profile_image_url_https"] as? String
}
let tweet = TweetDetails(authors: screenName, profileImages: avatarImage, tweetTexts: text, tweetLocations: city, tweetIDs: tweetID, userIDs: userID)
self.allTweets.append(tweet)
}
}
self.tweetTableView.reloadData()
}
} catch let jsonError as NSError {
print("json error: \(jsonError.localizedDescription)")
}
}

Related

Parse JSONArray contains JSONObjects Swift

hello i'm new to swift and I have responseJson from alamofire consist of jsonArray contain jsonObjects like this
[{"id":"1","name":"person1"},{"id":"2","name":"person2"}]
how i can parse it into array of this custom model
class Person {
var name : String
var id : String
}
i've done a much searching but can't find case identical to mine and i can't use Codable because i'm using xcode 8 and not able to upgrade my xcode version to 9 now
I'm getting the response like this
Alamofire.request(url).responseJSON{ response in
if(response.result.isSuccess)
{
if let jsonarray = response.result.value as? [[String: Any]]
{
//what to do here ?
}
}
}
if let jsonarray = response.result.value as? [[String: Any]]{
//what to do here ?
var persons:[Person] = []
for userDictionary in jsonarray{
guard let id = userDictionary["id"] as? String, let name = userDictionary["name"] as? String else { continue }
persons.append(Person(id, name))
}
//Persons complete.
}
Use guard else for the required variables.
If there are additional variables that could be optional, like var age:Int? in Person, you could do like this:
for userDictionary in jsonarray{
guard let id = userDictionary["id"] as? String, let name = userDictionary["name"] as? String else { continue }
let age = userDictionary["age"] as? Int
persons.append(Person(id, name, age))
}
#Tony,
In swift4 you can codable protocol to parsing the JSON that helps in writing generic code. Suppose if in future requirement came to add dob than its very simple.
And in swift3 you can use object mapper class for same.
If you need more help than please let me know.

Cannot force unwrap value of non-optional type 'AnyObject'

I've converted my iOS Project from swift 2.x to swift 3.x
There are now more then 50 errors in my code. One of the most common is this one "Cannot force unwrap value of non-optional type 'AnyObject'"
Here is a part of the code:
the line let documentURL = JSON[eachOne]["media"]!![eachMedia]["media_url"]! is producing the error
How can I resolve this problem? Thank you!
if let JSON = response.result.value as? [[String: AnyObject]]{
//print("JSON: \(JSON)")
myDefaultValues.userDefaults.setValue(JSON, forKey: "JSON")
for eachOne in 0 ..< (JSON as AnyObject).count{
// print("Cover: \(JSON[eachOne]["cover"])")
//Download all Covers
let documentURL = JSON[eachOne]["cover"]!
let pathus = URL(string: documentURL as! String)
if pathus != nil {
HttpDownloader.loadFileSync(pathus!, completion:{(path:String, error:NSError!) in
})
}
//Download all Media Files
if JSON[eachOne]["media"] != nil{
//print(JSON[eachOne]["media"]!)
//print(JSON[eachOne]["media"]!!.count)
let thisMediaView = JSON[eachOne]["media"]!.count
for eachMedia in 0 ..< thisMediaView!{
//print(JSON[eachOne]["media"]!![eachMedia]["media_url"])
**let documentURL = JSON[eachOne]["media"]!![eachMedia]["media_url"]!**
let pathus = URL(string: documentURL as! String)
if pathus != nil {
HttpDownloader.loadFileSync(pathus!, completion:{(path:String, error:NSError!) in
})
}
}
}
}
}
As a beginning Swift programmer you should pretend that the ! force unwrap operator doesn't exist. I call it the "crash if nil" operator. Instead, you should use if let or guard let optional binding. You cast your JSON object as a an array of dictionaries, so use the array directly:
for anObject in JSON {
guard let mediaArray = anObject["media"] as [[String: Any]] else
{
return
}
for aMediaObject in mediaArray {
guard let aMediaDict = aMediaObject as? [String: Any],
let documentURLString = aMediaDict["media_url"] as? String,
let url = URL(string: documentURLString ) else
{
return
}
//The code below is extracted from your question. It has multiple
//issues, like using force-unwrap, and the fact that it appears to be
//a synchronous network call?
HttpDownloader.loadFileSync(pathus!,
completion:{(path:String, error:NSError!) in {
}
}
}
That code might not be perfect, but it should give you the idea.
First of all, the following line in your code produces an Int, not an Array:
let thisMediaView = JSON[eachOne]["media"]!.count
Second, you could force-unwrap all your values, but that bring a lot of risk. You should not force-unwrap unless... no wait, just don't force-unwrap.
Your line here makes a lot of assumptions on the type of values that are in your JSON, without actually checking.
let documentURL = JSON[eachOne]["media"]!![eachMedia]["media_url"]!
To be a lot safer and more expressive, try to write it as follows:
if let value = JSON[eachOne] as? [String: Any],
let anotherValue = JSON[value] as? [String: Any],
...etc,
let documentURL = anotherValue["media_url"] as? String {
// do something with the values
} else {
// handle unexpected value or ignore
}

[<TestingAPI.Album 0x6100000ff300> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key albumId

I am new to iOS development and been trying to jump into swift straight away. I am trying to work with APIs and trying to learn myself. I've build this test Collection view along with the model to get the data however when I run the app I get a crash. Been trying to find a solution with no luck.
I've seen few that have the same crash however mostly due to a xib file which I am not using. I am building the app solely in code.
AlbumId
import UIKit
class AlbumId: NSObject {
var albumId: NSNumber?
static func fetchAlbums() {
let urlString = "https://jsonplaceholder.typicode.com/photos"
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print(error ?? "")
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers)
var albums = [Album]()
for dict in json as! [Any] {
let album = Album(dictionary: dict as! [String: Any])
album.setValuesForKeys(dict as! [String : Any])
albums.append(album)
}
} catch let err {
print(err)
}
}.resume()
}
}
Album
class Album: NSObject {
var id: NSNumber?
var title: String?
var url: String?
var thumbnailUrl: String?
init(dictionary: [String: Any]) {
super.init()
id = dictionary["id"] as? NSNumber
title = dictionary["title"] as? String
url = dictionary["url"] as? String
thumbnailUrl = dictionary["thumbnailUrl"] as? String
}
}
Your class Album does not have a property called albumId which means that it is not KVC compliant for that key.
It seems your JSON response has a key "albumId", but since your class is not KVC-compliant (it doesn't have an "albumId" property) using setValuesForKeys fails because setValuesForKeys requires that the instance must be KVC compliant for all of the keys in the dictionary.
Without a little knowledge regarding the JSON response, we can only make recommendations based on assumptions.
Your options are:
Change the property "id" to "albumId" on class Album
Change your API so the JSON key is simply "id"
Override setValueForKey and redirect "albumId" to your "id" property.
The error occurs because the model does not contain the property albumId.
Calling the KVC method setValuesForKeys is redundant anyway since you are initializing the object from the dictionary. There are only a few rare cases in Swift where KVC is useful. This is none of them. And inheritance from NSObject is actually not needed either.
The received JSON has id and albumId keys, so add the latter to the model and use Int rather than NSNumber. This code uses non-optional constants (let) with default values empty string / 0
class Album {
let albumId : Int
let id: Int
let title: String
let url: String
let thumbnailUrl: String
init(dictionary: [String: Any]) {
albumId = dictionary["albumId"] as? Int ?? 0
id = dictionary["id"] as? Int ?? 0
title = dictionary["title"] as? String ?? ""
url = dictionary["url"] as? String ?? ""
thumbnailUrl = dictionary["thumbnailUrl"] as? String ?? ""
}
}
Now populate the albums array, as always .mutableContainers is completely meaningless in Swift
if let json = try JSONSerialization.jsonObject(with: data!) as? [[String: Any]] else { return }
var albums = [Album]()
for dict in json {
let album = Album(dictionary: dict)
albums.append(album)
}
or in a swiftier way
var albums = json.map { Album(dictionary: $0) }

IOS swift how can i check for empty array while getting Json data

I am very new to swift and in my code I get JSON from a certain email, the JSON data is first put into an Array in swift, however if the array is null or there is no Json Data I get this error
Could not cast value of type 'NSNull' (0x1067a5748) to 'NSArray' (0x1067a4c58).
I been trying to fix this by reading other posts such as Check if optional array is empty but it is not working. The issue comes with this code below
session.dataTask(with:request, completionHandler: {(data, response, error) in
if error != nil {
print(error)
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
if let musicJob = parsedData["music"] as! [AnyObject]? {
for S in musicjob {
if let genre = S["genre"] as? String {
self.genres.append(genre)
}
}
}
If the Json is null I get the error on this line right here
if let musicJob = parsedData["music"] as! [AnyObject]?
The musicJob counts the number of objects such as "30 values or 44 values " etc . If there is no Json then it returns nil and it crashes . Is there anyway that I can catch the value of nil or an empty array so that the app does not crash ? Again everything works as long as the musicJob array is not empty .
Try this:
if let musicJob = parsedData["music"] as? [AnyObject]
You can check both nil and empty for Array like this.
if let musicJob = parsedData["music"] as? [Any], !musicJob.isEmpty {
print(musicJob)
}
Yes. You can always use conditional unwrapping. Force unwrapping is usually dangerous (crash) and should be used with great care.
First of all, using guard to make sure preconditions are met before doing anything is a good practice.
if guard let responseData = data else {
assertionFailure("response data is nil") // no need for assertionFailure is data is expected to be nil sometimes
return
}
And something similar with the line that crashes:
if let musicJob = parsedData["music"] as? [AnyObject] {
....
}

How to parse unknown json data type in swift 2

While fetching data from api I can get response either array of products or dictionary with error for e.g.
If everything went right api sends array of products as:
[
"Product1":
{
name = "someting",
price = 100,
discount = 10%,
images = [image1,image2]
},
"Product2":
{
name = "someting",
price = 100,
discount = 10%,
images = [image1,image2]
}
]
But if some error occur it sends dictionary with error message and code as:
{
error_message = "message"
error_code = 202
}
I am using this code to convert JSON data to array:
do {
let jsonDict = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSArray{
//Some code....
} catch let error as NSError {
print("JSON Error: \(error.localizedDescription)")
}
but if I get error as dictionary it crash.
Problems:
1. How to know whether received data is an array or dictionary ?
2. Some time even key or value can be missing so checking for value it becomes very lengthy code like:
if let productsArray = jsonObject as? NSArray{
if let product1 = productsArray[0] as? NSDictionary{
if let imagesArray = product1["image"] as? NSArray{
if let imageUrl = imagesArray[0] as? String{
//Code ....
}
}
}
}
I read about guard keyword to reduce if condition but I don't have clear idea how to use here.
Problem 1:
For try catch , add an if let for casting the object as NSDictionary or NSArray like :
do {
let jsonObject = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
if let jsonDict = jsonObject as? NSDictionary {
// Do smthg.
}
if let jsonArray = jsonObject as? NSArray {
// Do smthg.
}
}catch {
//...
}
For Problem 2:
I think guard won't help you . It needs smthg like return / break in its else statement. If you don't want to throw your methods if one of your values isn't available you have to use this lengthy if let code style.
Maybe in your case best practice would be setting up a Data Model for Product with optional properties.
Class product {
var name:String?
var image:[NSData]? // maybe UIImage or smthg.
var price:Int?
var discount:Int?
init(jsonDic:NSDictionary){
// if it's not there it would be nil
self.name = jsonDic["name"] as? String
self.image = jsonDic["image"] as? NSArray
self.discount = jsonDic["discount"] as? Int
self.price = jsonDic["price"] as? Int
}
}
Now you can load those models with your data without the if let etc..
But if you wanna read those values you have to use the if let for checkin if its not nil.
For init in your case it should be something like this:
Add this into the if let statement of the do catch block ( ... as? NSArray // DO smthg. )
for item in jsonArray {
guard let jsonDic = item as? NSDictionary else { return }
// if you dont know every key you can just iterate through this dictionary
for (_,value) in jsonDic {
guard let jsonDicValues = value as? NSDictionary else { return }
productArray.append(Product(jsonDic: jsonDicValues)
}
}
As i said , know you got the whole if let stuff when reading from the model an not when writing ( reading the json )
You have a few things going on here, one, I would analyze your server's http response status code and only attempt to process data if you received a status code indicating you will have good data
// In practical scenarios, this may be a range
if statusCode != 200 {
// Handle a scenario where you don't have good data.
return
}
Secondly, I'd guard against the response, it looks like you have named it "data" like so:
guard let receivedData = data else {
return
}
From this point on, you can use the receivedData constant.
Here'd I'd attempt to use NSJSONSeralization, like you do, but by casting it into a Swift dictionary, like so:
if let responseDictionary = try? NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? [String:AnyObject] {
// Here you can try to access keys on the response
// You can try things like
let products = responseDictionary?["products"] as? [[String:AnyObject]]
for product in products {
let productName = product["name"] as? String
if productName == nil {
continue
}
let newProduct = Product(name: productName)
// Do something with newly processed data
}
}
I tried to be general and also show you a guard example.
first of all I recommend using SwiftyJSON pod or the class straight into your Xcode, it works like a charm and you won't need to cast things down to figure whether you have a string or a dictionary or whatever. It is gold.
Once you've got your JSON, you can use this recursive function I created that does exactly what you need. It turns any Json into a dictionary. I mostly use it to save data into Firebase, without having to parse everything.
After you have imported SwiftyJSON into your project and added import SwiftyJSON to your Swift file you can:
//JSON is created using the awesome SwiftyJSON pod
func json2dic(_ j: JSON) -> [String:AnyObject] {
var post = [String:AnyObject]()
for (key, object) in j {
post[key] = object.stringValue as AnyObject
if object.stringValue == "" {
post[key] = json2dic(object) as AnyObject
}
}
return post
}
let json = JSON(value) // Value is the json structure you received from API.
var myDictionary = [String:AnyObject]()
myDictionary = json2dic(json)
You can catch the class of your response. If your response is kind of class dictionary, assign it as dictionary else if your response is kind of class array, assign it to array. Good luck.

Resources