15

I have an iOS app that needs to process a response from a web service. The response is a serialized JSON string containing a serialized JSON object, looking something like this:

"{ \"name\" : \"Bob\", \"age\" : 21 }"

Note that this response is a JSON string, not a JSON object. What I need to do is deserialize the string, so that I get this:

{ "name" : "Bob", "age" : 21 }

And then I can use +[NSJSONSerialization JSONObjectWithData:options:error:] to deserialize that into an NSDictionary.

But, how do I do that first step? That is, how to I "unescape" the string so that I have a serialized JSON object? +[NSJSONSerialization JSONObjectWithData:options:error:] only works if the top-level object is an array or a dictionary; it doesn't work on strings.

I ended up writing my own JSON string parser, which I hope conforms to section 2.5 of RFC 4627. But I suspect I've overlooked some easy way to do this using NSJSONSerialization or some other available method.

Community
  • 1
  • 1
Kristopher Johnson
  • 81,409
  • 55
  • 245
  • 302

4 Answers4

24

If you have nested JSON, then just call JSONObjectWithData twice:

NSString *string =  @"\"{ \\\"name\\\" : \\\"Bob\\\", \\\"age\\\" : 21 }\"";
// --> the string
// "{ \"name\" : \"Bob\", \"age\" : 21 }"

NSError *error;
NSString *outerJson = [NSJSONSerialization JSONObjectWithData:[string dataUsingEncoding:NSUTF8StringEncoding]
                              options:NSJSONReadingAllowFragments error:&error];
// --> the string
//  { "name" : "Bob", "age" : 21 }
NSDictionary *innerJson = [NSJSONSerialization JSONObjectWithData:[outerJson dataUsingEncoding:NSUTF8StringEncoding]
                              options:0 error:&error];
// --> the dictionary
// { age = 21; name = Bob; }
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • When I tried that, I got an error message. It seems to only work if the top-level object is an array or dictionary. – Kristopher Johnson Jun 05 '13 at 20:02
  • @KristopherJohnson: I tried exactly this code and it worked for me. You need the NSJSONReadingAllowFragments option in the first step. – Martin R Jun 05 '13 at 20:04
  • I swear I tried `NSJSONReadingAllowFragments` before, and it didn't work, but now it does. Thanks for making me try it again. – Kristopher Johnson Jun 05 '13 at 20:06
  • NSJSONReadingAllowFragments is not for that. It just allows to parse a single JSON String, or JSON number, or other JSON primitive. The string has to be a "proper" JSON string, that is it is escaped. (properly encoded). However, the given string seems to be a complete proper JSON -- just encoded in some special way. – CouchDeveloper Jun 05 '13 at 22:00
  • @CouchDeveloper: I don't quite understand your comment. `"{ \"name\" : \"Bob\", \"age\" : 21 }"` *is* the properly JSON encoded form of the string `{ "name" : "Bob", "age" : 21 }` - and the latter happens to be the JSON encoded form of a dictionary. Am I overlooking something? – Martin R Jun 06 '13 at 07:45
  • IFF the given string is actually a proper string of a JSON, then you are right! I'm just not sure whether the given string is actually encoded this way, and why: If you put a key/value pair into a JSON representation - whose value os a JSON, you would retrieve exactly that JSON after it went over the wire. That is if you would print NSLog("JSON: %@", dict[@"json"]) - it would print: __{ "name" : "Bob", "age" : 21 }__ – CouchDeveloper Jun 06 '13 at 10:20
  • Just another attempt to clarify: if you want to send any string - and JSON _is_ a string - via a JSON parser/decoder - then you NEVER will encode that yourself. Instead, you create a "JSON representation" (a hierarchical object structure) and pass this the JSON Parser/Decoder. On the receiver side, you would run the parser, and get some other platform JSON representation - say a Foundation hierarchy. When retrieving the string, that was initialized on the receiver side with as a JSON, you get exactly that back - excluding character encoding. – CouchDeveloper Jun 06 '13 at 10:30
0

Convert the string to data:

NSString *string = @"{ \"name\" : \"Bob\", \"age\" : 21 }";
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
Joe Hankin
  • 950
  • 8
  • 15
  • I do have an `NSData` object when I invoke `NSJSONSerialization`. So it is as if your code was `string = @"\"{ \\"name\\" : \\"Bob\\", \\"age\\" : 21 }\"";` – Kristopher Johnson Jun 05 '13 at 19:46
  • How are you getting that string? It sounds like somewhere along the pipeline it's being double-escaped, and that's what you need to debug. – Joe Hankin Jun 05 '13 at 19:52
  • 2
    The web service is double-escaping it. You and I may agree that it shouldn't, but it does, and I have to deal with it. – Kristopher Johnson Jun 05 '13 at 19:55
0

Just cut off the leading and trailing quotes and then replace all \"s with ":

NSString *sub = [original substringWithRange:(NSRange){ 1, original.length - 2 }];
NSString *unescaped = [sub stringByReplacingOccurrencesOfString:@"\\\" withString:@"\"];
  • That was my initial hacky workaround, but it only works if there are no other special characters in the string (`\n`, `\t`, `\u1234`, etc.). But I can't assume that. – Kristopher Johnson Jun 05 '13 at 19:52
  • A JSON string would require to escape these characters. So, when a encoder is applied, it would just escape the escape: "(\\n)" – CouchDeveloper Jun 05 '13 at 21:55
-1

One should first ask, why the server just don't include the JSON, as a sub structure.

But anyway. The string you got seems to be an escaped JSON. What that actually means, is totally up to the web service developer. I suspect, that just the double quotes and an escape itself have been escaped with an escape \. The resulting string is not "serialized" - JSON is already serialized - but encoded. In order to revert it back - you need to "unescape" or decode it again:

A little C++ snippet shows how (I know you asked for Objective-C -- but this is just too easy):

Edit: the code should also work for UTF-16 and UTF-32 -- with any endianness - and if the encoder just mechanically did what I suspect, it should also work for escaped unicode characters, e.g. \u1234 etc.

Edit - no, it won't work for UTF-16 and UTF-32. The sample would have to be fixed for that (which would be easy). But please ensure you have UTF-8 - which is almost always the case.

#include <iostream>

char input[] = u8R"___({ \"name\" : \"Bob\", \"age\" : 21 })___";

// Unescapes the character sequence "in-situ".
// Returns a pointer to "past-the-end" of the unescaped string.
static char* unescape(char* first, char* last) {
    char* dest = first;
    while (first != last) {
        if (*first == '\\') {
            ++first;
        }
        *dest++ = *first++;
    }
    return dest;
}

int main(int argc, const char * argv[])
{
    char* first = input;
    char* last = first + strlen(input);
    std::string s(input, unescape(first, last));

    std::cout << s << std::endl;

    return 0;
}

Prints:

{ "name" : "Bob", "age" : 21 }

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • 1
    Your example operates on the in input `{ \"name\" : \"Bob\", \"age\" : 21 }`. But the actual server response was (as I understand it) `"{ \"name\" : \"Bob\", \"age\" : 21 }"` (note the leading and trailing quotation mark). That's why NSJSONReadingAllowFragments works. – Martin R Jun 06 '13 at 07:52
  • OK, that means, NSJSONSerialization interprets this as a JSON string, and then returns this top level object - which is a NSString. So, effectively it applies a "JSON String decoder" to the given string. That might work - but I would anyway clarify how the given string has been decoded by the web service. – CouchDeveloper Jun 06 '13 at 09:57