//
you're reading...

Programming

Crash-proof your Cocoa-app when consuming JSON!

Consuming JSON data in your Cocoa application is JSON Cocoaa challenge of it’s own. It’s not just about using NSJSONSerializer and pulling data out of the NSArray / NSDictionary objects that you get out of it. There are a number of edge cases that you’ll need to consider:

  • What if you get a null value?
  • How do you ensure you will get a number object and not a stringified number?
  • How do you handle dates?

If any of those cases still haunt you at night, you’ll find your solution right here.

Handling NULL returns

When you read in a value from a JSON dictionary via [NSDictionary objectForKey:], you will encounter one of these cases:

  • You’ll get the value that you want – the happy case.
  • You’ll get a nil return – meaning that the key wasn’t there in the first place.
  • You’ll get an instance of NSNull – meaning that the key exists but the value is null.

It’s the last case that’s a bit puzzling for many Cocoa developers just starting out with JSON. However the protocol does allow null values to be returned. Here is an example JSON snippet which causes that last case:

{
    aKey: null
}

As you can see for yourself, it says that a key exists but the value is null. When you access that dictionary, the value of aKey would be the instance of NSNull (a singleton object), instead of nil. One potential use for that construct is to explicitly clear out a value or remove object relationships in the client side. 

But sometimes you might want to treat null and nil as equivalent. That is you just want either the number or string object that you wanted or nil otherwise – without caring for null conditions. An easy way to do this is to add some category methods to NSNull that returns nil so that you don’t need to explicitly check for NSNull object being returned to avoid “unrecognized selector sent to instance” exceptions.

@implementation NSNull (JSONSupport)

-(NSString*) stringValue {
    return nil;
}

-(NSNumber*) numberValue {
    return nil;
}

-(float) floatValue {
    return 0;
}

-(double) doubleValue {
    return 0;
}

-(BOOL) boolValue {
    return NO;
}

-(int) intValue {
    return 0;
}

-(long) longValue {
    return 0;
}

-(long long) longLongValue {
    return 0;
}

-(unsigned long long) unsignedLongLongValue {
    return 0;
}

@end

With the above methods in place, you can use clean constructs such as the following when accessing your JSON parser results:

NSDictionary* json = …
int numberOfThings = [json[@"things_count"] intValue];

 Which will safely return 0 and not throw an exception even if the server returns null for things_count.

Handling String / Number Conversions

Sometimes when you access a JSON value and you expect a number, you might get a string object instead. This could happen if the server decides to enclose the number between “quotes” – perhaps if it thinks that the number value is too large for you to handle. Similarly you might receive a unique identifier as a stringified number but you want to store it as a 64-bit integer for efficiency or speed (which makes sense if you’re saving the value into a local database).

Then you’ll need to coerce the value into the data type that you want, like so:

NSDictionary* json = …
unsigned long long uniqueID = [json[@"id"] unsignedLongLongValue];
NSString* foreignID = [json[@"foreign_id"] stringValue];
… 

The above works fine if you’re always sure that the server (and the JSON parser) will always return something in id and foreign_id fields that yields to an NSNumber instance. However it’s not a perfect world and there could be a chance that you’ll get a string instead. So it’s a good idea to mirror those methods that NSNumber has and add them to NSString too so that both of these classes have some more methods in common that’ll make them interchangeable:

@implementation NSString (JSONSupport)

-(NSString*) stringValue 
{
    return self;
}

-(unsigned long long) unsignedLongLongValue 
{
    return strtoull([self UTF8String], NULL,0);
}

-(NSNumber*) numberValue {
    NSString* trimmed = [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    
    NSRange locationOfDecimal = [trimmed rangeOfString:@"."];
    if (locationOfDecimal.location == NSNotFound) {
        // integer
        NSUInteger length = [trimmed length];
        if (length == 0) {
            return 0;
        }

        if (length >= 10) {
            // longer than +/- 2 147 483 647
            if ([trimmed hasPrefix:@"-"]) {
                return [NSNumber numberWithLongLong:[trimmed longLongValue]];
            } else {
                return [NSNumber numberWithUnsignedLongLong:[trimmed unsignedLongLongValue]];
            }
        } else if(length >= 5) {
            // ensure can be +/- 32 767
            if ([trimmed hasPrefix:@"-"]) {
                return [NSNumber numberWithInt:[trimmed intValue]];
            } else {
                return [NSNumber numberWithUnsignedInt:(unsigned int)[trimmed unsignedLongLongValue]];
            }
        } else if(length >= 3) {
            // ensure can be +/- 128
            if ([trimmed hasPrefix:@"-"]) {
                return [NSNumber numberWithShort:(short)[trimmed intValue]];
            } else {
                return [NSNumber numberWithUnsignedShort:(unsigned short)[trimmed unsignedLongLongValue]];
            }
        } else {
            if ([trimmed hasPrefix:@"-"]) {
                return [NSNumber numberWithChar:(char)[trimmed intValue]];
            } else {
                return [NSNumber numberWithUnsignedChar:(unsigned char)[trimmed unsignedLongLongValue]];
            }
        }
    } else {
        // floating-point
        return [NSNumber numberWithDouble:[trimmed doubleValue]];
    }
}
@end

@implementation NSNumber (JSONSupport)

-(NSNumber*) numberValue 
{
    return self;
}

@end 

Handling Date Values

JSON doesn’t say anything much on how to specify dates and timestamps. Most implementations expresses dates as human-readable strings, but there are several de facto prominent date formats that are being used. Even worse, the date format can even change without notification as what Twitter done back in 2007.

Facing this, you’ll need to be lenient and accept a number of popular date formats when parsing date strings that you’ve obtained in JSON. Fortunately I’ve identified a comprehensive set of date formats that occurs pretty commonly appearing in JSON and other human-readable Internet protocol data formats that you can readily use in your application:

@implementation NSDate (JSONSupport)

+(NSDate*) dateWithFuzzyString:(NSString*) dateString 
{
    if (!dateString) {
        return nil;
    }
    NSDate* result = nil;
    
#if !TARGET_OS_IPHONE
	result = [NSDate dateWithString:dateString];
	if (result) {
		return result;
	}
	result = [NSDate dateWithNaturalLanguageString:dateString];
	if (result) {
		return result;
	}
#endif
	
    // Unicode Technical Standard #35: Date Format Patterns
    // http://unicode.org/reports/tr35/tr35-10.html#Date_Format_Patterns (the page is really slow to load)
    // http://webcache.googleusercontent.com/search?q=cache:EzQS_WnAi1IJ:unicode.org/reports/tr35/tr35-10.html+cached:http://unicode.org/reports/tr35/tr35-10.html%23Date_Format_Patterns (alternative from google's cache).

    static NSString* const formats[] = {
        //Sample date: "Tue May 17 06:18:25 +0000 2011" (used by Twitter) 
        @"EEE MMM dd HH:mm:ss ZZZ yyyy",
        // sample date: Tue, 8 Dec 2009 21:30:43 +0800
        @"EEE, d MMM yyyy HH:mm:ss ZZZ",
        // sample date: Friday, 1 July 2001, 11:45 AM
        @"EEEE, d MMMM yyyy, hh:mm a",
        // sample date: 05/22/2007 03:15:23 UTC  (Twitter once switched to this format)
        // http://groups.google.com/group/twitter-development-talk/browse_thread/thread/0ed59aaaff01661c/80ef61a443768587
        @"dd/MM/yyyy HH:mm:ss zzz",
        // sample date: "December 10, 2009"
        @"MMMM, dd yyyy",
        // sample date: "Dec 10, 2009"
        @"MMM, dd yyyy",
        // sample date: 10-Dec-09
        @"dd-MMM-yy",
        // sample date: 10 Dec 09
        @"dd MMM yy",
    };
    
    
    NSDateFormatter* fmt= [NSDateFormatter new];
	fmt.formatterBehavior = NSDateFormatterBehavior10_4;
	[fmt setLenient:YES];
	for (NSUInteger i=0; i<ARRAY_COUNT(formats);i++) {
        fmt.dateFormat = formats[i];
		result = [fmt dateFromString:dateString];
		if (result) {
			return result;
		}
	}	
    return nil;    
}

-(NSString*) stringValue 
{
    // print date in MIME date format
    // http://tools.ietf.org/html/rfc5322#section-3.3
    // sample date: Tue, 8 Dec 2009 21:30:43 +0800

    NSDateFormatter* fmt= [NSDateFormatter new];
    fmt.dateFormat = @"EEE, d MMM yyyy HH:mm:ss ZZZ";     // sample date: Tue, 8 Dec 2009 21:30:43 +0800
    return [fmt stringFromDate:self];
}

@end

So that’s all there is to it. I’ve packaged all the code snippets above into a gist for you to use – open source under BSD license. Enjoy!

 



Do you enjoy this post? Enter your e-mail address below to receive articles like this one in your mailbox.

Free Updates!

Learn how to grow your indie business while keeping your day job.

Categories

Archives

Keep updated!

Don't miss out on new articles!