Why does SBJson JSON parsing only get the last key of interest? - iphone

I am using the following JSON: http://www.kb.dk/tekst/mobil/aabningstider_en.json
When I try to parse it by the key "location" as such:
// get response in the form of a utf-8 encoded json string
NSString *jsonString = [[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
// get most parent node from json string
NSDictionary *json = [jsonString JSONValue];
// get key-path from jason up to the point of json object
NSDictionary *locations = [json objectForKey:#"location"];
NSLog( #"%#", locations );
// iterate through all of the location objects in the json
for (NSDictionary *loc in locations )
{
// pull library name from the json object
NSString *name = [loc valueForKey:#"name"];
// add library data table arrays respectively
[ libraryNames addObject: ( ( name == nil | name.length > 0 ) ? name : #"UnNamed" ) ];
}
When I print the the object locations via NSLog:
{
address = "Universitetsparken 4, 3. etage, 2100 K\U00f8benhavn \U00d8";
desc = "";
lastUpdated = "";
latlng = "55.703124,12.559596";
link = "http://www.farma.ku.dk/index.php?id=3742";
name = "Faculty of Pharmaceutical Sciences Library";
parts = {
part = {
hour = {
day = "5.June Constitution Day (Denmark)";
open = Closed;
};
hours = {
hour = {
day = Friday;
open = "10-16";
};
};
name = main;
};
};
}
Which is only the last value for the "location" keys. Am I doing something wrong?
I tried validating the JSON via http://jsonlint.com/, however when I'd put in the JSON URL as above, it said "valid" - still only the last "locations" key was shown", however if I copy-paste it, it will not validate the JSON, and has to be fixed by removing new-lines from the string.
Also, when i try to parse the JSON and get the "name" fields, I get the following exception:
2012-05-08 15:37:04.941 iPhone App Tabbed[563:f803] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0x68bfe70> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'
*** First throw call stack:
(0x13dc052 0x156dd0a 0x13dbf11 0x9d2f0e 0x941841 0x940ca9 0x4593 0xf964e 0x114b89 0x1149bd 0x112f8a 0x112e2f 0x1148f4 0x13ddec9 0x365c2 0x3655a 0x25b569 0x13ddec9 0x365c2 0x3655a 0xdbb76 0xdc03f 0xdbbab 0x25dd1f 0x13ddec9 0x365c2 0x3655a 0xdbb76 0xdc03f 0xdb2fe 0x5ba30 0x5bc56 0x42384 0x35aa9 0x12c6fa9 0x13b01c5 0x1315022 0x131390a 0x1312db4 0x1312ccb 0x12c5879 0x12c593e 0x33a9b 0x281d 0x2785)
terminate called throwing an exception(lldb)
It would make more sense if the "locations" tag was an array object enclosed by square brackets ([]), however right now it's only an sequence of normal key-value pairs... Sadly, that's the JSON I have to work with.
Please help and thanks a great deal! :)
Sincerely,
Piotr.

The JSON you've got to work with may be valid, but it doesn't make much sense. It has one big dictionary with the location key repeated many times. Most JSON parser will simply return the last value for the repeated key. It would be best if you could change the structure to use an array instead, but if you cannot there's still hope. You can read the stream and stuff the values from the location keys into an array as they come out of it. This is how you'd do that:
#interface BadJsonHelper : NSObject
#property(strong) NSMutableArray *accumulator;
#end
#implementation BadJsonHelper
- (void)parser:(SBJsonStreamParser *)parser foundArray:(NSArray *)array {
// void
}
- (void)parser:(SBJsonStreamParser *)parser foundObject:(NSDictionary *)dict {
[accumulator addObject:dict];
}
#end
You can drop that little helper class at the top of your file, outside the #implementation section of the class where you're doing your work. (There's no need for the #interface and #implementation being in different files.)
In your code, you would use it like this:
BadJsonHelper *helper = [[BadJsonHelper alloc] init];
helper.accumulator = [NSMutableArray array];
SBJsonStreamParserAdapter *adapter = [[SBJsonStreamParserAdapter new] init];
adapter.delegate = helper;
adapter.levelsToSkip = 1;
SBJsonStreamParser *parser = [[SBJsonStreamParser alloc] init];
parser.delegate = adapter;
switch ([parser parse: responseData]) {
case SBJsonStreamParserComplete:
NSLog(#"%#", helper.accumulator);
break;
case SBJsonStreamParserWaitingForData:
NSLog(#"Didn't get all the JSON yet...");
break;
case SBJsonStreamParserError:
NSLog(#"Error: %#", parser.error);
break;
}
This example was originally adapted from the following test:
https://github.com/stig/json-framework/blob/master/Tests/StreamParserIntegrationTest.m
Update: I created a fully functional example project that loads the JSON asynchronously and parses it. This is available from github.

The JSON is valid, however there is a basic problem regarding the definition of the array of items.
Instead of defining an array of locations using brackets, the JSON redefines the same location key/value pair over and over again. In other words JSON initially says the value of location is the collection with name "The Black Diamond", but immediately after it redefines it with the collection with name "Faculty Library of Humanities" and so on till the last location Faculty of Pharmaceutical Sciences Library".
The same is true for parts and hours.
If you can't fix the result of the JSON and you really need to get it working you may want to modify the JSON removing the "location" keys and adding brackets properly.
Edit
Alternatively you may use an NSScanner and process the JSON result manually. Kinda hacky but it will work as long as the JSON format doesn't change significantly.
Edit
This snipped of code should do the work...
NSString *jsonString = [[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
int indx = 1;
for (;;)
{
NSRange locationRange = [jsonString rangeOfString:#"\"location\":"];
if (locationRange.location == NSNotFound) break;
jsonString = [jsonString stringByReplacingCharactersInRange:locationRange
withString:[NSString stringWithFormat:#"\"location%d\":", indx++]];
}

NSDictionary *locations = [json objectForKey:#"location"];
As you can see, the result of JSON parsing by SBJson is a NSDictionary. A dictionary contains key/value pairs, and the keys are unique identifiers for the pairs.
The JSON data you need to handle is valid but not a good one. Per RFC 4627 - 2.2:
An object structure is represented as a pair of curly brackets surrounding zero or more name/value pairs (or members). A name is a string. A single colon comes after each name, separating the name from the value. A single comma separates a value from a following name. The names within an object SHOULD be unique.
Things like jQuery can parse the JSON also, but the result is the same as SBJson (the last one as the one). See Do JSON keys need to be unique?.
It is not a MUST, but it's still not a good practice. It would be much easier if you are able to change the structure of the JSON data on the server side (or even on the client side after receiving it) rather than parsing it as is.

Related

Present JSON string in sorted order from NSDictionary

I made one JSON string from the NSDictionary. The JSON string which I created doesn't come present the items in the order I entered keys and value pair in the NSDictionary .
Now I need all keys and value pairs returned as JSON string in alphabetic order. I tried lot of ways but not getting sorting of JSON string in alphabetic order.
Here's an example of the way the final string is being presented:
{
"SubscriberID" : "603",
"Amount" : "1",
"MerchantID" : "100012",
"Channel" : "Wallet",
"RequestCode" : "0331",
"PosID" : "0465F35F5577CUST",
"TID" : "0000014",
"Stan" : "NA"
}
How do I ensure the items are presented the way I entered them? Or how can I specify the items are alphabetically sorted?
SBJson is able to do this, set sortKeys to YES on SBJsonWriter
http://superloopy.io/json-framework/
SBJsonWriter *writer = [[SBJsonWriter alloc] init];
writer.sortKeys = YES;
// Turn on humanReadable if you also need it pretty-printed
// writer.humanReadable = YES;
NSData *result = [writer dataWithObject:myDictionary];
Like many key-value storage classes, NSDictionary does not guarantee order of the elements you've added. It is a mistake to assume that the key/value pairs, or keyset, will be returned with any particular order.
Further, JSON objects are unordered in the same fashion. You should not care about the order in which the objects are added. Nor should you (or your recipient) rely on being provided a JSON object with 'sorted' keys, as that's not really a valid concept with these particular structures. While ordering the keys might result in an iterator traversing the keys in the order you expect, it's not a guarantee, and should not be relied on.
I think you should revisit why you need the keys sorted in the first place, and see if you can find a way to avoid a dependency on their alphabetical ordering.
Edit: You mention the server requires an SHA hash of the JSON string. If you must, you can create a sorted JSON string by sorting the keys in an NSMutableArray, then creating the JSON string from those keys.
NSMutableArray *sortedKeys = [NSMutableArray arrayWithArray:[myDict allKeys]];
[sortedKeys sortedArrayUsingSelector:#selector(caseInsensitiveCompare:)];
NSMutableString *jsonString = [[NSMutableString alloc] init];
[jsonString appendString:#"{"];
for (NSString *key in sortedKeys) {
[jsonString appendFormat:#"\"%#\"", key];
[jsonString appendString:#":"];
[jsonString appendFormat:#"\"%#\"", [myDict objectForKey:key]];
[jsonString appendString:#","];
}
if ([jsonString length] > 2) {
[jsonString deleteCharactersInRange:NSMakeRange([jsonString length] - 1, 1)];
}
[jsonString appendString:#"}"];
This code hasn't been tested, so you might need to play with it a bit. Thought it would be much better if you could find a JSON library to do this for you, although you might not have as much control over the key ordering.
You can't do it (or at least you can't rely on that). JSON objects are unordered sets as you can read on JSON.org.
Actually,the system method [NSString stringWithFormat:] is what you need.So here is the easy way:
NSDictionary* inputDictionary = #{
#"SubscriberID" : #"603",
#"Amount" : #"1",
#"MerchantID" : #"100012",
#"Channel" : #"Wallet",
#"RequestCode" : #"0331",
#"PosID" : #"0465F35F5577CUST",
#"TID" : #"0000014",
#"Stan" : #"NA"
};
NSString* sortedJsonStr = [NSString stringWithFormat:#"%#", inputDictionary];
NSLog(#"sorted json string is %#", sortedJsonStr);
The result is:
{
Amount = 1;
Channel = Wallet;
MerchantID = 100012;
PosID = 0465F35F5577CUST;
RequestCode = 0331;
Stan = NA;
SubscriberID = 603;
TID = 0000014;
}
The question does not make sense, and therefore cannot be answered in its current form.
If you encode some JSON as {"keyB":"valueB","keyA":"valueA"}, take it's checksum, and then transmit the encoded JSON and the checksum to the remote site, the remote site has little choice but to take the checksum of the JSON as received and compare that. To do the checksum on sorted values it would have to decode the received JSON string into an NSDictionary, re-encode into "sorted" JSON, and then take the checksum of the reconstituted JSON, and that would be a lot of extra effort for no reason.
Far more likely is that there is some difference in either the way the checksum is being computed (padding of the SHA256 input, eg) or some difference in the JSON strings being used -- code page differences, escaped characters, one end is doing it on Base64 and the other not, etc.

Is this NSDictionary valid the way it is set up? Using XML to NSDictionary plugin

I'm using the XMLReader plugin found here https://github.com/Insert-Witty-Name/XML-to-NSDictionary to convert my XML data into an NS-Dictionary, but I am confused by how the dictionary is being set up. Here is what I am given:
{
response = {
"#status" = ok;
authentication = {
"#description" = "The username you provided is valid.";
"#login" = USERNAME;
"#response" = success;
"#user_id" = USERID;
};
};
}
I am trying to take the response object and say if key is equal to success then do something, but I'm not sure if this dictionary is even set up correctly.
Despite the weirdness of the #names as keys, it seems valid.
You could easily ask for your element response ( [ objectForKey:#"response"] ), get the [ objectForKey:#"authentication"] and then [ objectForKey:#"#response"] to check [yourString isEqualToString:#"success"].
Edit: Adding example
I've recently noticed some ; where commas should have been, but let's hope that's just a typo or something like it.
In case you have a doubt, you can always NSLog(#"%#", [yourFirstDictionary allKeys]); to make sure those are valid keys.
Let's call your first object myDict for the sake of the example.
NSDictionary * response = [myDict objectForKey:#"response"]; // this should have #status and authentication as keys
NSDictionary * authentication = [response objectForKey:#"authentication"];
NSString * innerResponse = [authentication objectForKey:#"#response"];
if ([innerResponse isEqualToString:#"success"]) {
// your code
}

parsing JSON object and sub-elements on iphone app

I am building an app that uses a UPC data base API. I am getting back a JSON object, example from here: http://www.simpleupc.com/api/methods/FetchNutritionFactsByUPC.php
{
"success":true,
"usedExternal":false,
"result"
{
"calories_per_serving":"150",
"cholesterol_per_serving":"15",
"cholesterol_uom":"Mg",
"dvp_calcium":"30",
"dvp_cholesterol":"4",
"dvp_iron":"2",
"dvp_protein":"17",
"dvp_saturated_fat":"8",
"dvp_sodium":"10",
"dvp_total_fat":"4",
"dvp_vitamin_a":"10","
"dvp_vitamin_c":"0",
"dvp_vitamin_d":"25",
"fat_calories_per_serving":"25",
"fiber_per_serving":"<1",
"fiber_uom":"G",
"ingredients":"Fat Free Milk, Milk, Sugar, Cocoa (Processed With Alkali),
Salt, Carrageenan, Vanillin (Artificial Flavor),
Lactase Enzyme, Vitamin A Palmitate And Vitamin D3.",
"protein_per_serving":"8",
"protein_uom":"G",
"size":"240",
"units":"mL",
"servings_per_container":"8",
"sodium_per_serving":"230",
"sodium_uom":"Mg",
"total_fat_per_serving":"2.5",
"total_fat_uom":"G",
"trans_fat_per_serving":"0",
"trans_fat_uom":"G",
"upc":"041383096013"
}
}
My problem is with parsing the "ingredients" element, which is a sub list of the object dictionary.
How would you suggest parsing the ingredients list? If I could get it to an NSArray, assuming commas are separators, that would have been great.
I tried to do this, but looks like its just a string, so no way to parse it.
Any suggestion would be more than welcome. Thanks!
//Thats the whole JSON object
NSDictionary *json_dict = [theResponseString JSONValue];
//Getting "results" which has all the product info
NSArray *myArray = [[NSArray alloc] init];
myArray = [json_dict valueForKey:#"result"];
Now how do I get "ingredients" from myArray in an array form?
You're getting result as an array, but (in JSON terminology) it's not an array. It's an object, so use an NSDictionary. Something like this:1
NSDictionary *result = [json_dict objectForKey:#"result"];
Then you can get the inner ingredients object from that:
NSString *ingredients = [result objectForKey:#"ingredients"];
Edited as per #Bavarious' comment.
1Apologies for glaring errors, as I'm not terribly well-versed in Objective-C. You might need to allocate memory for the returned NSDictionary and NSString pointers; I'm not sure.
Here's all you need to do:
NSDictionary *json_dict = [theResponseString JSONValue];
// Use a key path to access the nested element.
NSArray *myArray = [json_dict valueForKeyPath:#"result.ingredients"];
EDIT
Whoops, Matt's right. Here's how to deal with the string value:
// Use a key path to access the nested element.
NSString *s = [json_dict valueForKeyPath:#"result.ingredients"];
NSArray *ingredients = [s componentsSeparatedByString:#", "];
Note that you might need to trim the '.' character off the last element of the array.

Get objects from a NSDictionary

I get from an URL this result :
NSString *result = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
it looks like this :
[{"modele":"Audi TT Coup\u00e9 2.0 TFSI","modele_annee":null,"annee":"2007","cilindre":"4 cyl","boite":"BVM","transmision":"Traction","carburant":"ES"},
{"modele":"Audi TT Coup\u00e9 2.0 TFSI","modele_annee":null,"annee":"2007","cilindre":"4 cyl","boite":"BVM","transmision":"Traction","carburant":"ES"}]
So it contains 2 dictionaries. I need to take the objects from all the keys from this result. How can I do this?
I tried this : NSDictionary vehiculesPossedeDictionary=(NSDictionary *)result;
and then this : [vehiculesPossedeDictinary objectForKey:#"modele"]; but this is not working.
Please help me... Thanks in advance
What you have is a JSON string which describes an "array" containing two "objects". This needs to be converted to Objective-C objects using a JSON parser, and when converted will be an NSArray containing two NSDictionarys.
You aren't going to be able to get your dictionary directly from a string of JSON. You are going to have to going to have to run it through a JSON parser first.
At this point, there is not one build into the iOS SDK, so you will have to download a third-party tool and include it in your project.
There are a number of different JSON parser, include TouchJSON, YAJL, etc. that you can find and compare. Personally, I am using JSONKit.
#MatthewGillingham suggests JSONKit. I imagine it does fine, but I've always used its competitor json-framework. No real reason, I just found it first and learned it first. I do think its interface is somewhat simpler, but plenty of people do fine with JSONKit too.
Using json-framework:
require JSON.h
...and then
NSString *myJsonString = #"[{'whatever': 'this contains'}, {'whatever', 'more content'}]";
NSArray *data = [myJsonString JSONValue];
foreach (NSDictionary *item in data) {
NSString *val = [item objectForKey:#"whatever"];
//val will contain "this contains" on the first time through
//this loop, then "more content" the second time.
}
If you have array of dictionary just assign objects in array to dictionary like
NSDictionary *dictionary = [array objectAtIndes:0];
and then use this dictionary to get values.

iPhone: variable type returned by yajl

I'm quite new to iphone programming and I want to do the following stuff:
get data from a JSON REST web server
parse the received data using YAJL
Draw a graph with those data using core-plot
So, 1th item is fine, I use ASIHttpRequest which runs as espected
3rd is almost fine (I still have to learn how to tune core-plot).
The problem I have is regarding 2nd item.
I use YAJL as it seems to be the faster parser, so why not give it a try :)
Here is the part of code that gets the data from the server and parse them:
// Get server data
response_data = [request responseData];
// Parse JSON received
self.arrayFromData = [response_data yajl_JSON];
NSLog(#"Array from data: %#", self.arrayFromData);
The parsing works quite well in fact, the NSLog output is something like:
2010-06-14 17:56:35.375 TEST_APP[3733:207] Array from data :
{
data = (
{
val = 1317;
date = "2010-06-10T15:50:01+02:00";
},
{
val = 1573;
date = "2010-06-10T16:20:01+02:00";
},
........
{
val = 840;
date = "2010-06-11T14:50:01+02:00";
},
{
val = 1265;
date = "2010-06-11T15:20:01+02:00";
}
);
from = "2010-06-10T15:50:01+02:00";
to = "2010-06-11T15:20:01+02:00";
max = "2590";
}
According to th yajl-objc explanations http://github.com/gabriel/yajl-objc, the parsing returns a NSArray...
The thing is... I do not know how to get all the values from it as for me it looks more like a NSDictionary than a NSArray...
Could you please help ?
Thanks a lot,
Luc
edit1: it happens that this object is actually a NSCFDictionary (!), I am still not able to get value from it, when I try the objectFromKey method (that should work on a Dictionary, no ?) it fails.
It's returning an NSDictionary. NSCFDictionary is a private subclass and is immaterial to this discussion. So it looks like you'd retrieve stuff like:
NSDictionary * responseDictionary = ...;
NSArray * dataArray = [responseDictionary objectForKey:#"data"];
for (NSDictionary * dataPair in dataArray) {
NSLog(#"val: %#, date: %#", [dataPair objectForKey:#"val"], [dataPair objectForKey:#"date"]);
}