We're using AFNetworking in our mobile app and a lot of times we will have JSON come back that has null for some values.
I'm getting tired of doing the following.
if ([json objectForKey:#"nickname"] isKindOfClass:[NSNull class]]) {
nickname = nil;
} else {
nickname = [json objectForKey:#"nickname"];
}
Anything we can do to make AFNetworking automagically set objects to nil or numbers to 0 if the value is null in the JSON response?
You can set flag setRemovesKeysWithNullValues to YES in AFHTTPSessionManager response serializer:
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]initWithBaseURL:url sessionConfiguration:config];
AFJSONResponseSerializer *serializer = [AFJSONResponseSerializer serializer];
[serializer setRemovesKeysWithNullValues:YES];
[manager setResponseSerializer:serializer];
It's not really possible, since the dictionary can't contain nil as the object for a key. The key would have to be left out entirely in order to get the behavior you'd want, which would be undesirable in its own way.
Suppose you didn't have control over the data you were receiving and didn't know what keys were present in the JSON. If you wanted to list them all, or display them in a table, and the keys for null objects were left out of the dictionary, you'd be seeing an incorrect list.
NSNull is the "nothing" placeholder for Cocoa collections, and that's why it's used in this case.
You could make your typing a bit easier with a macro:
#define nilOrJSONObjectForKey(JSON_, KEY_) [[JSON_ objectForKey:KEY_] isKindOfClass:[NSNull class]] ? nil : [JSON_ objectForKey:KEY_]
nickname = nilOrJSONObjectForKey(json, #"nickname");
DV_'s answer works great for AFHTTPSessionManager. But if you are using AFHTTPRequestOperation instead of the manager, try this:
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
AFJSONResponseSerializer *serializer = [AFJSONResponseSerializer serializer];
serializer.removesKeysWithNullValues = YES;
op.responseSerializer = serializer;
There is one beautiful cocoapod called Minced https://github.com/hyperoslo/Minced that can do something that can help you handle NULL from JSON response. Instead of NULL it puts empty string.
If you replace the default NSJSONSerialization with SBJSON it will solve your problem.
SBJSON makes objects nil instead of NSJSONSerialization's choice of "null"
look at the requirements for the different JSON parsers you can use.
https://github.com/AFNetworking/AFNetworking#requirements
You can custom AFNetworking at this functions. set any value default to objects that is NULL
static id AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) {
if ([JSONObject isKindOfClass:[NSArray class]]) {
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]];
for (id value in (NSArray *)JSONObject) {
[mutableArray addObject:AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)];
}
return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];
} else if ([JSONObject isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];
for (id <NSCopying> key in [(NSDictionary *)JSONObject allKeys]) {
id value = (NSDictionary *)JSONObject[key];
if (!value || [value isEqual:[NSNull null]]) {
// custom code here
//[mutableDictionary removeObjectForKey:key];
[mutableDictionary setObject:#"" forKey:key];
} else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {
mutableDictionary[key] = AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions);
}
}
return (readingOptions & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary];
}
return JSONObject;
}
Related
I am getting the following crash on crashlytics.
Fatal Exception: NSUnknownKeyException
[<__NSCFString 0x1742aeb80> valueForUndefinedKey:]: this class is not key value coding-compliant for the key url.
This is where the crash occurred for some of the users.
id url = [[json[#"data"]valueForKey:#"value"]valueForKey:#"url"];
I'm not sure what is the best way to prevent this crash. I believe this is because json[#"data"] is an NSString in certain cases. So I believe I should check if this is an NSDictionary like this.
if ([json[#"data"] isKindOfClass:[NSDictionary class]]) {
id url = [[json[#"data"]valueForKey:#"value"]valueForKey:#"url"];
}
Any tips or suggestions are appreciated.
This is my end result after getting answers from here. Does this look okay? I didn't include all my code at first to keep things simple.
if ([json isKindOfClass:[NSDictionary class]]) {
id url = nil;
id type = nil;
NSDictionary *data = json[#"data"];
if ([data isKindOfClass:[NSDictionary class]]) {
type = data[#"type"];
NSDictionary *value = data[#"value"];
if ([value isKindOfClass:[NSArray class]]) {
url = [value valueForKey:#"url"];
}
if ([type isKindOfClass:[NSString class]] && [url isKindOfClass:[NSArray class]] && [url count] != 0) {
// do stuff
}
}
}
You should check NSDictionary one by one to prevent crash. Try my code below
NSDictionary *dictionary = json[#"data"];
NSString *output = #"";
if ([dictionary isKindOfClass:[NSDictionary class]]) {
dictionary = dictionary[#"value"];
if ([dictionary isKindOfClass:[NSDictionary class]]) {
output = dictionary[#"url"];
}
}
NSLog(#"%#", output);
You got crash because of calling valueForKey method on a NSString value. If someone says the reason for crash is call valueForKey when dictionary doesn't have this key, it's wrong. For more information Sending a message to nil in Objective-C
[dictionary isKindOfClass:[NSDictionary class]] always return NO if dictionary is nil so don't need to check dictionary in if statement. It's unnessary.
Your error means that json[#"data"]valueForKey:#"value"] doesn't NSDictionarry, so it have no #"url" key.
valueForKey it's KVC method, use objectForKey for dictionaries, and add more checks like:
id url = nil;
NSDictionary *data = [json objectForkey:#"data"];
if ([data isKindOfClass:[NSDictionary class]]) {
NSDictionary *value = [data objectForKey:#"value"];
if ([value isKindOfClass:[NSDictionary class]]) {
url = [value objectForKey:#"url"];
}
}
Why NSDictionary cannot be written?? I have checked the content of the dictionary: all the instances are of NSString and NSNumber. I checked permissions: a text file with the same name at the same path is written well. Of course, my dictionary is not empty.
NSString *file = ...
NSDictionary *dict = ...
// check dictionary keys
BOOL wrong = NO;
for (id num in [dict allKeys]) {
if (![num isKindOfClass:[NSNumber class]]) {
wrong = YES;
break;
}
}
if (wrong) {
NSLog(#"First");
}
// check dictionary values
wrong = NO;
for (id num in [dict allValues]) {
if (![num isKindOfClass:[NSString class]]) {
wrong = YES;
break;
}
}
if (wrong) {
NSLog(#"Second");
}
if (![dict writeToFile:file atomically:YES]) {
// 0k, let's try to create a text file
NSLog(#"Names writing error!");
[#"Something here... .. ." writeToFile:file atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
Output: "Names writing error!"
Text file is created successfully.
Writing out a dictionary creates a property list, and according to the documentation all keys in a property list must be strings.
... and although NSDictionary and CFDictionary objects allow their keys to
be objects of any type, if the keys are not string objects, the
collections are not property-list objects.
NSNumber objects as keys are not supported.
As #vadian points out, you cannot write plist with numeric keys. But you can use NSKeyedArchiver:
NSURL *documents = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:false error:nil];
NSURL *fileURL = [documents URLByAppendingPathComponent:#"test.plist"];
// this will not work
NSDictionary *dictionary = #{#1: #"foo", #2: #"bar"};
BOOL success = [dictionary writeToFile:fileURL.path atomically:true];
NSLog(#"plist %#", success ? #"success" : #"failure");
// this will
fileURL = [documents URLByAppendingPathComponent:#"test.bplist"];
success = [NSKeyedArchiver archiveRootObject:dictionary toFile:fileURL.path];
NSLog(#"archive %#", success ? #"success" : #"failure");
And you can read it back with NSKeyedUnarchiver:
// to read it back
NSDictionary *dictionary2 = [NSKeyedUnarchiver unarchiveObjectWithFile:fileURL.path];
NSLog(#"dictionary2 = %#", dictionary2);
Note, you can do this with any class that conforms (and properly implements) NSCoding. Fortunately, NSDictionary conforms already. You have to make sure that any objects inside the dictionary, also conform (both NSString and NSNumber do). If you had a custom object in your dictionary, you'd have to make it properly conform yourself.
This is all described in the Archives and Serializations Programming Guide.
I'm evaluating RestKit to use in my project. I've created a simple app that loads some JSON and maps it into Objective-C objects. I'm having a problem correctly mapping a JSON object that has numeric and logical fields. E.g.
{
"integerValue":"5",
"booleanValue":"YES",
}
I want these to map to the following properties in my data object:
#property int integerValue;
#property BOOL booleanValue;
It didn't work out of the box, so I've created a value transformer for that:
[_activityMapping setValueTransformer:[RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class inputValueClass, __unsafe_unretained Class outputValueClass) {
if([inputValueClass isSubclassOfClass:[NSString class]] && [outputValueClass isSubclassOfClass:[NSNumber class]]) {
return YES;
}
else {
return NO;
}
} transformationBlock:^BOOL(id inputValue, __autoreleasing id *outputValue, __unsafe_unretained Class outputClass, NSError *__autoreleasing *error) {
if([[inputValue class] isSubclassOfClass:[NSString class]] && [outputClass isSubclassOfClass:[NSNumber class]]) {
NSString *inputString = (NSString *)inputValue;
if([inputString isEqualToString:#"YES"] || [inputString isEqualToString:#"NO"]) {
*outputValue = [NSNumber numberWithBool:[inputString boolValue]];
}
else {
*outputValue = [NSNumber numberWithInt:[inputString intValue]];
}
}
else {
*outputValue = [inputValue copy];
}
return YES;
}]];
This code works, but looks ugly. Note how I have to check the input value to see if it's a boolean or an integer. Any suggestions on an elegant solution to this problem?
Please note that I'm using RestKit. I do know about NSJSONSerialization and know how to parse JSON in code. If you suggest a non-RestKit solution, please explain why do you not recommend using RestKit.
The issue is not occurring at the RestKit level but at the JSON level itself.
According to the JSON spec Boolean values should be represented with true/false not YES/NO. If you update your JSON to be semantically correct then RestKit should do the right thing.
Ok. So according to my understanding of your answer, your main problem lies in mapping the data in the JSON object to their very own designated variables.
So, I'd recommend using the conventional NSJSONSerialization approach.
So, first up. You need to store your JSON object in an NSData object. Now, you're most likely downloading the data from a simple URL. So, this is what you'd do :
//This part is just to download the data. If you're using another method - that's fine. Just make sure that the download is in NSData format
NSURL *url = [[NSURL alloc] initWithString : #"YOUR_URL_HERE"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL : url];
NSData *jsonData = [NSURLConnection sendSynchronousRequest:request
returningResponse:nil
error:nil];
Now, you need to map those to the NSDictionary... Here's how :
//This is the actual NSJSONSerialization part.
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableLeaves
error:nil];
Now, just map the values to your designated properties.
_integerValue = (int)[jsonDict objectForKey:#"integerValue"];
_booleanValue = (BOOL)[jsonDict objectForKey:#"booleanValue"];
I'm a developer from Python world used to using exceptions. I found in many places that using exceptions is not so wise here, and did my best to convert to NSErrors when needed. but then I encounter this:
NSMutableArray *results;
for (NSDictionary *dict in dicts)
{
// Memory management code omitted
SomeModel *model = [[SomeModel alloc] init];
model.attr1 = [[dict objectForKey:#"key1"] integerValue];
model.attr2 = [[dict objectForKey:#"key2"] integerValue];
model.attr3 = [[dict objectForKey:#"key3"] integerValue];
model.attr4 = [[dict objectForKey:#"key4"] integerValue];
[results addObject:model];
}
with some of the objects in dict containing NSNull, which would result an "unrecognized selector" exception. In that case, I want to drop that datum completely. My first instinct is to wrap the whole content of the for block into a #try-#catch block:
NSMutableArray *results;
for (NSDictionary *dict in dicts)
{
#try
{
SomeModel *model = [[SomeModel alloc] init];
model.attr1 = [[dict objectForKey:#"key1"] integerValue];
model.attr2 = [[dict objectForKey:#"key2"] integerValue];
model.attr3 = [[dict objectForKey:#"key3"] integerValue];
model.attr4 = [[dict objectForKey:#"key4"] integerValue];
[results addObject:model];
}
#catch(NSException *exception)
{
// Do something
}
}
But is this a good approach? I can't come up with a solution without repeating checks on each variable, which is really ugly IMO. Hopefully there are alternatives to this that haven't occur to me. Thanks in advance.
The proper Objective-C way to do this would be:
for (NSDictionary *dict in dicts)
{
if (! [dict isKindOfClass:[NSDictionary class]])
continue;
// ...
}
Testing if a receiver can respond to a message before sending it is a typical pattern in Objective-C.
Also, take note that exceptions in Objective-C are always a programmer error and are not used for normal execution flow.
Many people use a category on NSDictionary for these cases:
- (id)safeObjectForKey:(id)aKey
{
id obj = [self objectForKey:aKey];
if ([obj isKindOfClass:[NSNull class]])
{
return nil;
}
return obj;
}
You still need to make sure, that your dict is an actual dictionary instance.
In the end I decided to solve the problem using KVC. Something like this:
- (id)initWithPropertyDictionary:(NSDictionary *)dict
lookUpTable:(NSDictionary *)keyToProperty
{
self = [self init];
for (NSString *key in dict)
{
NSString *propertyName;
if ([keyToProperty objectForKey:key])
propertyName = [keyToProperty objectForKey:key];
else
propertyName = key;
if ([[dict objectForKey:key] isKindOfClass:[NSNull class]])
{
[self release];
return nil;
}
else
{
[self setValue:[dict objectForKey:key] forKey:propertyName];
}
}
}
The setback of this resolution is that I'll have to use NSNumber for my properties, but for JSON data there is really no distinction between floating numbers and integers, so this is fine.
And if you really want primitive types, you can couple this method with custom setters that converts those NSNumbers into appropriate types.
With this, all you need to do is check for nil before adding the object into the array. Much cleaner everywhere except the model class.
Thanks to jaydee3 for inspiring me to focus on changing the model class.
I'm setting values for properties of my NSManagedObject, these values are coming from a NSDictionary properly serialized from a JSON file. My problem is, that, when some value is [NSNull null], I can't assign directly to the property:
fight.winnerID = [dict objectForKey:#"winner"];
this throws a NSInvalidArgumentException
"winnerID"; desired type = NSString; given type = NSNull; value = <null>;
I could easily check the value for [NSNull null] and assign nil instead:
fight.winnerID = [dict objectForKey:#"winner"] == [NSNull null] ? nil : [dict objectForKey:#"winner"];
But I think this is not elegant and gets messy with lots of properties to set.
Also, this gets harder when dealing with NSNumber properties:
fight.round = [NSNumber numberWithUnsignedInteger:[[dict valueForKey:#"round"] unsignedIntegerValue]]
The NSInvalidArgumentException is now:
[NSNull unsignedIntegerValue]: unrecognized selector sent to instance
In this case I have to treat [dict valueForKey:#"round"] before making an NSUInteger value of it. And the one line solution is gone.
I tried making a #try #catch block, but as soon as the first value is caught, it jumps the whole #try block and the next properties are ignored.
Is there a better way to handle [NSNull null] or perhaps make this entirely different but easier?
It might be a little easier if you wrap this in a macro:
#define NULL_TO_NIL(obj) ({ __typeof__ (obj) __obj = (obj); __obj == [NSNull null] ? nil : obj; })
Then you can write things like
fight.winnerID = NULL_TO_NIL([dict objectForKey:#"winner"]);
Alternatively you can pre-process your dictionary and replace all NSNulls with nil before even trying to stuff it into your managed object.
Ok, I've just woke up this morning with a good solution. What about this:
Serialize the JSON using the option to receive Mutable Arrays and Dictionaries:
NSMutableDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:_receivedData options:NSJSONReadingMutableContainers error:&error];
...
Get a set of keys that have [NSNull null] values from the leafDict:
NSSet *nullSet = [leafDict keysOfEntriesWithOptions:NSEnumerationConcurrent passingTest:^BOOL(id key, id obj, BOOL *stop) {
return [obj isEqual:[NSNull null]] ? YES : NO;
}];
Remove the filtered properties from your Mutable leafDict:
[leafDict removeObjectsForKeys:[nullSet allObjects]];
Now when you call fight.winnerID = [dict objectForKey:#"winner"]; winnerID is automatically going to be (null) or nil as opposed to <null> or [NSNull null].
Not relative to this, but I also noticed that it is better to use a NSNumberFormatter when parsing strings to NSNumber, the way I was doing was getting integerValue from a nil string, this gives me an undesired NSNumber of 0, when I actually wanted it to be nil.
Before:
// when [leafDict valueForKey:#"round"] == nil
fight.round = [NSNumber numberWithInteger:[[leafDict valueForKey:#"round"] integerValue]]
// Result: fight.round = 0
After:
__autoreleasing NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
fight.round = [numberFormatter numberFromString:[leafDict valueForKey:#"round"]];
// Result: fight.round = nil
I wrote a couple of category methods to strip nulls from a JSON-generated dictionary or array prior to use:
#implementation NSMutableArray (StripNulls)
- (void)stripNullValues
{
for (int i = [self count] - 1; i >= 0; i--)
{
id value = [self objectAtIndex:i];
if (value == [NSNull null])
{
[self removeObjectAtIndex:i];
}
else if ([value isKindOfClass:[NSArray class]] ||
[value isKindOfClass:[NSDictionary class]])
{
if (![value respondsToSelector:#selector(setObject:forKey:)] &&
![value respondsToSelector:#selector(addObject:)])
{
value = [value mutableCopy];
[self replaceObjectAtIndex:i withObject:value];
}
[value stripNullValues];
}
}
}
#end
#implementation NSMutableDictionary (StripNulls)
- (void)stripNullValues
{
for (NSString *key in [self allKeys])
{
id value = [self objectForKey:key];
if (value == [NSNull null])
{
[self removeObjectForKey:key];
}
else if ([value isKindOfClass:[NSArray class]] ||
[value isKindOfClass:[NSDictionary class]])
{
if (![value respondsToSelector:#selector(setObject:forKey:)] &&
![value respondsToSelector:#selector(addObject:)])
{
value = [value mutableCopy];
[self setObject:value forKey:key];
}
[value stripNullValues];
}
}
}
#end
It would be nice if the standard JSON parsing libs had this behaviour by default - it's almost always preferable to omit null objects than to include them as NSNulls.
Another method is
-[NSObject setValuesForKeysWithDictionary:]
In this scenario you could do
[fight setValuesForKeysWithDictionary:dict];
In the header NSKeyValueCoding.h it defines that "Dictionary entries whose values are NSNull result in -setValue:nil forKey:key messages being sent to the receiver.
The only downside is you will have to transform any keys in the dictionary to keys that are in the receiver. i.e.
dict[#"winnerID"] = dict[#"winner"];
[dict removeObjectForKey:#"winner"];
I was stuck with the same problem, found this post, did it in a slightly different way.Using category only though -
Make a new category file for "NSDictionary" and add this one method -
#implementation NSDictionary (SuperExtras)
- (id)objectForKey_NoNSNULL:(id)aKey
{
id result = [self objectForKey:aKey];
if(result==[NSNull null])
{
return nil;
}
return result;
}
#end
Later on to use it in code, for properties that can have NSNULL in them just use it this way -
newUser.email = [loopdict objectForKey_NoNSNULL:#"email"];
Thats it