11
Jan
2012
 

Handling incoming JSON redux

by Tom Harrington

A few months ago I wrote here about a generic approach to safely take incoming JSON and save values to Core Data object. The goals of that code were twofold:

  1. Provide a safe, generic alternative to Cocoa’s -setValuesForKeysWithDictionary: for use with NSManagedObject and its subclasses
  2. Handle cases where JSON data didn’t match up with what the managed objects expected. Getting a string where you expect a numeric value, or vice versa, for example, or getting a string representation of a date when you want a real NSDate object.

The first item was the most important. It’s tempting to use -setValuesForKeysWithDictionary: to transfer JSON to a model object in one step. The method runs through the dictionary and calls -setValue:forKey: on the target object for every entry. It has a fatal flaw though, in that it doesn’t check to see if the target object actually has a key before trying to set it. Using this method when you don’t have absolute control over the dictionary contents is an invitation to unknown key exceptions and nasty app crashes.

Fixing this for managed objects was relatively easy because Core Data provides convenient Objective-C introspection methods. The general approach was:

  • Get a list of the target object’s attributes
  • For each attribute, see if the incoming dictionary has an entry. If so,
    • Compare the incoming type to the expected type, and convert if necessary.
    • Call -setValue:forKey: with that key and its value.

And then just last week I had the thought, wouldn’t it be nice if this worked for any object, not just for managed objects?

Objective-C introspection

Since Objective-C is dynamic, pretty much everything you’d want to know about a class is available at run time. I’m not just talking about methods like -respondsToSelector: and -isKindOfClass:, though those are extremely useful. You can go much, much deeper than that, inspecting (and even changing) every aspect of a class’s implementation. Much of this happens via C function calls rather than Objective-C method calls. The Objective-C runtime is not actually written in Objective-C, and it’s the runtime that has the information.

To update the code for use with objects that don’t inherit from NSManagedObject the new code looks through the properties declared on a class and uses those to run through the incoming JSON. The general approach is the same but the implementation uses NSObject properties instead of NSEntityDescription attributes.

It’s also possible to look through the instance variables instead of the properties. Often this would amount to the same thing. Where they differ is when the backing ivar has a different name, i.e. when you’re using something like:

@synthesize foo = __myReallyBizarrePrivateName____;

In that case iterating over properties would find foo while iterating over instance variables would find __myReallyBizarrePrivateName____. Either is valid but I’m going with the properties because (at least for me) they’re more likely to match up with the JSON keys.

First pass: iterating over properties

A simple version that meets requirement #1 looks like this:

- (void)setValuesForKeysWithJSONDictionary:(NSDictionary *)keyedValues dateFormatter:(NSDateFormatter *)dateFormatter
{
    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList([self class], &propertyCount);
    
    for (int i=0; i

This starts with a call to class_copyPropertyList(), which gets a C-style array of property declarations for the requested class. The propertyCount argument indicates how many properties are in the array. The array contains zero or more objc_property_t entries, which is an opaque structure.

The code iterates through this array. For each one it uses property_getName to get the property name as a C-style string. Then it converts this to an NSString and uses that to look up entries in the incoming dictionary. And, voila, we're using the class's own properties to look up values in the dictionary instead of the other way around.

A final detail-- unusual in Objective-C code-- is the call to free(). Since class_copyPropertyList() has copy in its name, the calling code is responsible for disposing of the returned data. And since it's a C call, this needs to be done C style. This call would need to be there even if the project were using ARC.

Back to Core Data, briefly

The great thing about this solution is that it's not the non-managed-object alternative to the previous version, it's a direct replacement. This approach works just as well on managed objects as on other objects-- provided, that is, that you create custom subclasses of NSManagedObject for your entities that declare properties for managed object attributes. So long as the properties exist, the code works. If you're using Xcode or mogenerator to generate your managed object subclasses, you're covered. If you aren't creating custom subclasses, first of all, why not? But in that case this approach won't work since NSManagedObject doesn't have the necessary property declarations.

Does it have to be like this?

Some of you may have noticed that it's possible to do the same thing without any mucking about with the runtime by doing something like this:

    for (NSString *key in keyedValues) {
        @try {
            [self setValue:[keyedValues objectForKey:key] forKey:key];
        }
        @catch (NSException *exception) {
            // Do nothing
        }
    }

In this case the dictionary keys still drive the action, but exception handling means the code doesn't crash on unknown keys. So why bother then? Because of requirement #2 above. Coercing JSON into appropriate data types is going to require introspection. With this simplified approach you don't crash, but you also don't get type conversions. If not crashing is all you're interested in, this works just as well and is probably faster. It's certainly simpler anyway. It's not going to do what I need though.

JSON Fixes

To meet requirement #2 the code needs to go deeper. As with the Core Data implementation, it needs to look up the expected value for the property and compare that to the type of the incoming data. To do this I'll expand the if block beginning on line 12 above to look like this:

if (value != nil) {
    char *typeEncoding = NULL;
    typeEncoding = property_copyAttributeValue(property, "T");
    
    if (typeEncoding == NULL) {
        continue;
    }
    switch (typeEncoding[0]) {
        case '@':
        {
            // Object
            Class class = nil;
            if (strlen(typeEncoding) >= 3) {
                char *className = strndup(typeEncoding+2, strlen(typeEncoding)-3);
                class = NSClassFromString([NSString stringWithUTF8String:className]);
            }
            // Check for type mismatch, attempt to compensate
            if ([class isSubclassOfClass:[NSString class]] && [value isKindOfClass:[NSNumber class]]) {
                value = [value stringValue];
            } else if ([class isSubclassOfClass:[NSNumber class]] && [value isKindOfClass:[NSString class]]) {
                // If the ivar is an NSNumber we really can't tell if it's intended as an integer, float, etc.
                NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
                [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
                value = [numberFormatter numberFromString:value];
                [numberFormatter release];
            } else if ([class isSubclassOfClass:[NSDate class]] && [value isKindOfClass:[NSString class]] && (dateFormatter != nil)) {
                value = [dateFormatter dateFromString:value];
            }
            
            break;
        }
            
        default:
        {
            break;
        }
    }
    [self setValue:value forKey:keyName];
    free(typeEncoding);
}

The runtime provides information about properties via the call to property_copyAttributeValue() on line 3. The first argument is the property of interest. The second one can have a bunch of different values depending on what you want to look up. A "T" requests a C-style string that describes the property type. For full details on what you can do with the second argument, see the (somewhat out of date as of this writing) "Declared Properties" section of Apple's Objective-C Runtime Programming Guide.

If the property type is a pointer to a class, the return value will be something like T@"NSNumber", T@"NSString", T@"MyClass", etc. The next thing the code does then is to check for a leading @ and, if found, set about getting the Class that the type string names. This happens in lines 12-16. The code strips off the @ and the quotes, converts to NSString, and uses NSClassFromString to get the Class.

The rest of this code is remarkably similar to the Core Data version, looking for type mismatches and converting the incoming value where needed. The chief difference is that it uses -isSubclassOfClass: to check on the expected type of the property instead of looking at the Core Data-specific NSAttributeType.

Except for one important difference. For numeric properties, the Core Data attribute type would indicate whether a floating point or integer value was expected. With NSNumber we have no way of knowing. So the code uses NSNumberFormatter to parse incoming strings and leaves it at that. If this kind of mismatch occurs then there are bigger problems anyway. You could change the code to round a float to an int, but is that actually going to be a valid result? Maybe, maybe not.

The type comparisons in this code could go on forever but in this case the code is specifically looking for problems that sometimes crop up with JSON.

Since property_copyAttributeValue() has copy in its name, this block adds another call to free() to clean up after itself.

Handling primitives

But what if the expected value is not an object at all? What if it's a primitive int? Fortunately property_copyAttributeValue() covers that case as well. In this case the type encoding string is shorter, with values like Ti for int, Tf for float, TQ for unsigned long long, etc (again, see Apple's docs for a full list).

With this information, the switch statement above can be expanded with a few more cases.

        case 'i': // int
        case 's': // short
        case 'l': // long
        case 'q': // long long
        case 'I': // unsigned int
        case 'S': // unsigned short
        case 'L': // unsigned long
        case 'Q': // unsigned long long
        case 'f': // float
        case 'd': // double
        case 'B': // BOOL
        {
            if ([value isKindOfClass:[NSString class]]) {
                NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
                [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
                value = [numberFormatter numberFromString:value];
                [numberFormatter release];
            }
            break;
        }
            
        case 'c': // char
        case 'C': // unsigned char
        {
            if ([value isKindOfClass:[NSString class]]) {
                char firstCharacter = [value characterAtIndex:0];
                value = [NSNumber numberWithChar:firstCharacter];
            }
            break;
        }

The first half of this, up to line 20, handles all the numeric types. As with the previous code block there's no attempt to work out floating point/integer conflicts since it's impossible to know how to handle this in the general case. It would be possible to break this up into more specific checks, say by using -[NSString intValue] when an integer result is expected and hope for valid data. But the code above handles the case where you actually have an integer value, and if you don't have one then again, you have bigger problems.

The rest of this block handles a primitive char property by taking the first character in the incoming string. Longer strings can't be stored in a char anyway, so this is the best approach.

In both cases the conversion results in an NSNumber instead of a primitive type. That's OK though-- -setValue:forKey: has our back here and will unbox the object for us.

Conclusion

Using this approach makes it a lot easier to deal with web services. You can't always rely on the results matching what you expect (or what's documented). Inspecting classes at run time takes a more defensive approach to dealing with data you can't control.

A category on NSObject that implements this code can be found at github.