RickiG RickiG - 2 months ago 20
JSON Question

Object Mapping library from JSON to NSObjects

I am trying to build a parser/objectMapper that will build Objective C objects for the JSON I consume from a REST service.

I took some inspiration from RestKit by having my Entities all hold a "decoding list" which tells a mapper which JSON keys goes with which objects. Like this:

//ObjectEntity implementation
+ (NSDictionary*) mapProperties {

/*
localPropertiy - JSONProperty
*/

return @{
@"name": @"name",
@"category": @"category",
@"possible_scopes": @"possibleScopes",
@"possible_descriptions": @"possibleDescriptions",
@"key": @"keys"
};

}

+ (NSDictionary*) mapRelations {

return [NSDictionary dictionary];
}


I did so because I like the encapsulation of these changeable values to be in the object that they reference. Making the Mapper know as little as possible.

The mapper does something like this:

+ (NSArray*) parseData:(NSData*) jsonData intoObjectsOfType:(Class) objectClass {

//Parser result from web service
NSError *error = nil;
CJSONDeserializer *deserializer = [CJSONDeserializer deserializer];
[deserializer setNullObject:nil];
NSArray *objects = [deserializer deserializeAsArray:jsonData error:&error];

NSMutableArray *result = [NSMutableArray array];

for (NSDictionary *o in objects) {

id <EntityProtocol> entity = [[objectClass alloc] init];

NSDictionary *jsonKeys = objectClass.mapProperties;

for (NSString *key in jsonKeys.allKeys) {

NSString *objectProperty = jsonKeys[key];
NSString *value = o[key];
if (value)
[entity setValue:value forKey:objectProperty];
}

[result addObject:entity];

}

return (NSArray*)result;
}


So I message the parser/mapper like this:

NSArray *objects = [ObjectParser parseData:self.responseData intoObjectsOfType:ObjectEntity.class];


This means that the parser must know what my root object is, which is fine as the object retrieving it from the web service of course has this knowledge.

The above only works for JSON without nested objects, I have been trying to build the parser so that it will take the relationships into account as well, building the needed objects and inserting them into the root object, this needs to be recursive and I keep running into dead ends.

I would like some help to how I could approach this or any insight to as if something like this exists out as a library. Maybe for using or maybe just for tackling the parts I have problems with.

Thank you in advance.

Answer

Why not add mappings for classes?

+ (NSDictionary *)mapClasses {
    return @{
            @"category": [Category class],
            // ...
    };
}

For non-container properties, you could even do runtime introspection of properties to avoid redundant mappings.

Container properties could map to special wrapper objects:

[OMContainer arrayWithClass:Key.class], @"keys",
[OMContainer dicionaryWithKeyClass:ScopeID.class valueClass:Scope.class], @"possibleScopes",

Or even blocks for dynamic selection of types:

[OMDynamicType typeWithBlock:^(id obj){
    if ([obj isKindOfClass:NSString.class] && [obj hasPrefix:@"foo"])
        return Foo.class;
    else
        return Bar.class;
}], @"foo", 

Implementing this could go something like:

+ (NSArray *)parseData:(NSData*)jsonData intoObjectsOfType:(Class)objectClass {
    NSArray *parsed = /* ... */
    NSMutableArray *decoded = [NSMutableArray array];
    for (id obj in parsed) {
        [decoded addObject:[self decodeRawObject:parsed intoObjectOfType:objectClass]];
    }
    return decoded;
}

+ (id)decodeRawObject:(NSDictionary *)dict intoObjectOfType:(Class)objectClass {
    // ...
    NSDictionary *jsonKeys = objectClass.mapProperties;
    NSDictionary *jsonClasses = objectClass.mapClasses;

    for (NSString *key in jsonKeys.allKeys) {
        NSString *objectProperty = jsonKeys[key];
        NSString *value = dict[key];
        if (value) {
            id klass = jsonClasses[key];
            if (!klass) {
                [entity setValue:value forKey:objectProperty];
            } else if (klass == klass.class) {
                [entity setValue:[self decodeRawObject:value intoObjectOfType:klass]
                          forKey:objectProperty];
            } else if (/* check for containers and blocks */) {
                // ...
            }
        }
    }
    // ...
}