Search code examples
iosjsonmodel-view-controllermodeljsonmodel

JSONModel iOS and Polymorphism


Using the following models as examples, what are the best practices of handling polymorphism within JSONModel?

@interface GameModel : JSONModel
@property (nonatomic, assign) long id;
@property (nonatomic, assign) NSArray<GameEventModel> *events;
/*
  ...
*/
@end

@interface GameEventModel : JSONModel
@property (nonatomic, assign) long long timestamp;
/*
  ...
*/
@end

@interface GameTouchEventModel : GameEventModel
@property (nonatomic, assign) CGPoint point;
/*
  ...
*/
@end

When GameModel is initiated with a JSON string of {id:1, events:[{point:{x:1, y:1}, timestamp:...}]}

JSONModel will use the GameEventModel and ignore the point property.

Would it be better to use a generic GameEventModel which contains a type property and info property such as...

@interface GameTouchEventModel : GameEventModel
@property (nonatomic, strong) NSString *type;
@property (nonatomic, strong) NSDictionary *info;
@end

And therefore the model could accept JSON as {id:1, events:[{ type:"GameTouchEventModel", info:{ point:{x:1, y:1}, timestamp:... } }]}

The problem with this approach is harder to read code and no compiler warnings/errors amongst others.

Is there no way to use polymorphic models in JSONModel?


Solution

  • We solved this with 2 minor alterations to JSONModel.m, introducing a new special JSON property __subclass which is picked up by the JSONModel parser and uses the value as the object type. __subclass is required to be a reserved keyword (therefore no models can use __subclass as a property name).

    Alterations to JSONModel.m

    // ...
    -(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err
    {
          // ...
          if ([self __isJSONModelSubClass:property.type]) {
    
                //initialize the property's model, store it
                JSONModelError* initErr = nil;
    
                -- id value = [[property.type alloc] initWithDictionary: jsonValue error:&initErr];
    
                ++ id value;
                ++ if([jsonValue valueForKey:@"subclass"] != NULL)
                ++ {
                ++       Class jsonSubclass = NSClassFromString([d valueForKey:@"subclass"]);
                ++       if(jsonSubclass)
                ++             obj = [[jsonSubclass alloc] initWithDictionary:d error:&initErr];
                ++ }
                ++ else
                ++     value = [[property.type alloc] initWithDictionary: jsonValue error:&initErr];
           //...
    //...
    +(NSMutableArray*)arrayOfModelsFromDictionaries:(NSArray*)array error:(NSError**)err
    {
          // ...
          for (NSDictionary* d in array) {
               JSONModelError* initErr = nil;
    
               -- id obj = [[self alloc] initWithDictionary:d error:&initErr];
    
               ++ id obj;
               ++ if([d valueForKey:@"subclass"] != NULL)
               ++ {
               ++       Class jsonSubclass = NSClassFromString([d valueForKey:@"subclass"]);
               ++       if(jsonSubclass)
               ++             obj = [[jsonSubclass alloc] initWithDictionary:d error:&initErr];
               ++ }
               ++ else
               ++      obj = [[self alloc] initWithDictionary:d error:&initErr];
           // ...
     // ...
    

    NOTE: If the _subclass'ed JSON model class doesn't exist, then the model will fallback to the superclass.

    This will then work with the following models

    @interface GameModel : JSONModel
    @property (nonatomic, assign) long id;
    @property (nonatomic, assign) NSArray<GameEventModel> *events;
    @end
    
    @protocol GameEventModel
    @end
    
    @interface GameEventModel : JSONModel
    @property (nonatomic, assign) long long timestamp;
    @end
    
    @interface GameTouchEventModel : GameEventModel
    @property (nonatomic, strong) NSArray *point;
    @end
    

    When passed the JSON string {id:1, events:[ { __subclass:'GameTouchEventModel', timestamp:1, point: [0,0] } ] }