RestKit: mapping BOOL and integer values - objective-c

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"];

Related

NSDictionary writeToFile fails while objects are valid, permission is 0k

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.

Converting a JSON file to NSMutableDictionary in Objective C?

I have a json file that looks like this:
{
"data":
{
"level": [
{
//bunch of stuff
}
]
}
}
Now I want to convert that into a array of levels that I can access. If I take away the {"data: part, then I can use this:
NSData *allLevelsData = [[NSData alloc] initWithContentsOfFile:fileLoc];
NSError *error = nil;
NSMutableDictionary *allLevels = [NSJSONSerialization JSONObjectWithData:allLevelsData options:kNilOptions error:&error];
if(!error){
NSMutableArray *level = allLevels[#"level"];
for (NSMutableDictionary *aLevel in level){
//do stuff with the level...
But I have to have the {"data: as part of the file, and I can't figure out how to get a NSData object out of the existing NSData object. Any ideas?
Don't you need to pull the level NSArray out of the data NSDictionary first?
NSData *allLevelsData = [[NSData alloc] initWithContentsOfFile:fileLoc];
NSError *error = nil;
NSDictionary *dataDictionary = [NSJSONSerialization JSONObjectWithData:allLevelsData options:kNilOptions error:&error];
if(!error){
NSArray *levels = dataDictionary[#"data"][#"level"];
for (NSDictionary *aLevel in levels){
//do stuff with the level...
You won't get mutable objects back by default and declaring the variables as mutable doesn't make them so. Take a mutableCopy of the result instead (assuming you really do need mutability).
Why are you trying to prune ahead of time? If you decode the original JSON, you'll be able to extract the level array from the data dict in the decoded dict.
It's not clear what else you're trying to accomplish or why you are going the path you ask about. Note, this doesn't necessarily mean your path is wrong, just that without a clearer indication of what goal you're really trying to accomplish or what you've actually tried (and errored/failed, along with how it failed), you're likely only to get vague/general answers like this.

Filtering Parsed JSON in Objective-C

I'm trying to take out the "lasttradeprice" in https://www.allcrypt.com/api.php?method=singlemarketdata&marketid=672 but I can't seem to figure out how to grab the "lasttradeprice" piece.
How would I 'filter' the "price" out? None of the other information is relevant.
Current Code:
NSURL * url=[NSURL URLWithString:#"https://www.allcrypt.com/api.php?method=singlemarketdata&marketid=672"]; // pass your URL Here.
NSData * data=[NSData dataWithContentsOfURL:url];
NSError * error;
NSMutableDictionary * json = [NSJSONSerialization JSONObjectWithData:data options: NSJSONReadingMutableContainers error: &error];
NSLog(#"%#",json);
NSMutableArray * referanceArray=[[NSMutableArray alloc]init];
NSMutableArray * periodArray=[[NSMutableArray alloc]init];
NSArray * responseArr = json[#"lasttradeprice"];
for(NSDictionary * dict in responseArr)
{
[referanceArray addObject:[dict valueForKey:#"lasttradeprice"]];
[periodArray addObject:[dict valueForKey:#"lasttradeprice"]];
}
NSLog(#"%#",referanceArray);
NSLog(#"%#",periodArray);
NOTE: Keep in mind I've never worked with JSON before so please keep your answers dumbed down a tad.
Key value coding provides an easy way to dig through that data. Use the key path for the values you want. For example, it looks like you could get the array of recent trades using the path "return.markets.OMC.recenttrades" like this (assuming your code to get the json dictionary):
NSArray *trades = [json valueForKeyPath:#"return.markets.OMC.recenttrades"];
That's a lot more concise than having to dig down one level at a time.
The value returned for a given key by an array is the array of values returned by the array's members for that key. In other words, you can do this:
NSArray *recentprices = [trades valueForKey:#"price"];
And since that's just the next step in the key path, you can combine the two operations above into one:
NSArray *recentprices = [json valueforKeyPath:#"return.markets.OMC.recenttrades.price"];
The only down side here is that there's no real error checking -- either the data matches your expectations and you get back your array of prices, or it doesn't match at some level and you get nil. That's fine in some cases, not so much in others.
Putting that together with the relevant part of your code, we get:
NSURL *url = [NSURL URLWithString:#"https://www.allcrypt.com/api.php?method=singlemarketdata&marketid=672"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSError *error = nil;
NSMutableDictionary *json = [NSJSONSerialization JSONObjectWithData:data options: NSJSONReadingMutableContainers error:&error];
NSArray *recentprices = [json valueforKeyPath:#"return.markets.OMC.recenttrades.price"];
Update: I just noticed that you want the "lasttradeprice", not the array of prices. Given that, the key path to use is simply #"return.markets.OMC.lasttradeprice", and the value you'll get back will be a string. So replace the last line above with:
NSString *lastTradePrice = [json valueforKeyPath:#"return.markets.OMC.lasttradeprice"];
The value you want is buried a few dictionaries deep. One general idea might be to dig recursively, something like this:
- (BOOL)isCollection:(id)object {
return [object isKindOfClass:[NSArray self]] || [object isKindOfClass:[NSDictionary self]];
}
- (void)valuesForDeepKey:(id)key in:(id)collection results:(NSMutableArray *)results {
if ([collection isKindOfClass:[NSDictionary self]]) {
NSDictionary *dictionary = (NSDictionary *)collection;
if (dictionary[key]) [results addObject:dictionary[key]];
for (id deeperKey in [dictionary allKeys]) {
if ([self isCollection:dictionary[deeperKey]]) {
[self valuesForDeepKey:key in:dictionary[deeperKey] results:results];
}
}
} else if ([collection isKindOfClass:[NSArray self]]) {
NSArray *array = (NSArray *)collection;
for (id object in array) {
if ([self isCollection:object]) {
[self valuesForDeepKey:key in:object results:results];
}
}
}
}
Then call it like this:
NSMutableArray *a = [NSMutableArray array];
[self valuesForDeepKey:#"lasttradeprice" in:json results:a];
NSLog(#"%#", a);

Can I get AFNetworking to automatically parse NULL to nil?

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

How to return a raw string of text/json from aspnet mvc and parse it with the iPhone JSON library

I have a web service that returns a raw string of JSON data (sample below)
{'d':{'success':true,'msg':null,'data':[{'productId':'4b7fcb0f818e4a4abaf5b3c78654b631','id':4283}]}}
... And when I attempt to parse this JSON using the popular JSON library for iPhone development I notice a problem in the following method
- (id)objectWithString:(NSString *)repr {
[self clearErrorTrace];
if (!repr) {
[self addErrorWithCode:EINPUT description:#"Input was 'nil'"];
return nil;
}
depth = 0;
c = [repr UTF8String];
id o;
if (![self scanValue:&o]) {
return nil;
}
// more code ...
}
When I hit the last if statement shown I show c to be a valid char (showing the json values inside as expected) but I notice that after o is defined I return nil inside that last if causing this error from the library
#import "NSString+SBJSON.h"
#import "SBJsonParser.h"
#implementation NSString (NSString_SBJSON)
- (id)JSONValue
{
SBJsonParser *jsonParser = [SBJsonParser new];
id repr = [jsonParser objectWithString:self];
if (!repr)
NSLog(#"-JSONValue failed. Error trace is: %#", [jsonParser errorTrace]);
[jsonParser release];
return repr;
}
#end
Any idea what might be wrong with taking a string of what looks like json and trying to parse it like the below?
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[connection release];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
[responseData release];
NSArray *json = [responseString JSONValue]; //here it all fails
}
Just as a note- this works without error when I query a valid json web service from a large company. My app is aspnet mvc and the return line is simply
return this.Content(jsonStringValue, "text/json");
You should use the built in JSON result object from your action method instead of the Content() method:
return Json(data);
It will automatically serialize your object to be valid JSON.
It would be useful to provide actual error messages/traces.
At any rate, your JSON string is not valid: a string in JSON is quoted within double quotes, not single quotes. The following is valid JSON:
{"d":{"success":true,"msg":null,"data":[{"productId":"4b7fcb0f818e4a4abaf5b3c78654b631","id":4283}]}}
Edit: note that your JSON (top level) data represents an object, not an array. Hence
NSArray *json = [responseString JSONValue];
should be replaced with
NSDictionary *json = [responseString JSONValue];
because SBJSON represents JSON objects as dictionaries.