Question

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?

Was it helpful?

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] } ] }

OTHER TIPS

I think BWJSONMatcher can handle it in a very neat way.

Declare your model as follows:

@interface GameModel : NSObject<BWJSONValueObject>
@property (nonatomic, assign) long id;
@property (nonatomic, strong) NSArray *events;
@end

@interface GameEventModel : NSObject
@property (nonatomic, assign) long long timestamp;
@end

@interface GameTouchEventModel : GameEventModel
@property (nonatomic, strong) NSDictionary *point;
@end

In the implementation of GameModel, implement this function:

- (Class)typeInProperty:(NSString *)property {
    if ([property isEqualToString:@"events"]) {
        return [GameEventModel class];
    }

    return nil;
}

And then your can get your own data instance from json string within one line:

GameModel *gameModel = [GameModel fromJSONString:jsonString];

The examples about how to use BWJSONMatcher to handle polymorphism can be found here.

TL;DR

Using Swagger could help, see this example in github.

About Swagger

The accepted solution is one way, but I'd like to offer an alternative. If you generate the models using Swagger and take advantage of the "allOf/discriminator" feature to implement inheritance, the generated Objective-C class will contain similar code to the one offered by the accepted solution.

Yaml

definitions:
  Point:
    type: object
    properties:
      x:
       type: number
      y:
       type: number

  GameEventModel:
    type: object
    discriminator: gameEventModelType

  GameTouchEventModel:
    type: object
    description: GameTouchEventModel
    allOf:
      - $ref: '#/definitions/GameEventModel'
      - type: object
        properties:
          gameEventModelType:
            type: string
          point:
             $ref: '#/definitions/Point'

  GameFooEventModel:
    type: object
    description: GameTouchEventModel
    allOf:
      - $ref: '#/definitions/GameEventModel'
      - type: object
        properties:
          gameEventModelType:
            type: string
          name:
             type: string

  GameModel:
    type: object
    properties:
      id:
        type: integer
        format: int64
      events:
        type: array
        items:
          $ref: '#/definitions/GameEventModel'

Generated code snippet

/**
 Maps "discriminator" value to the sub-class name, so that inheritance is supported.
 */
- (id)initWithDictionary:(NSDictionary *)dict error:(NSError *__autoreleasing *)err {


    NSString * discriminatedClassName = [dict valueForKey:@"gameEventModelType"];

    if(discriminatedClassName == nil ){
         return [super initWithDictionary:dict error:err];
    }

    Class class = NSClassFromString([@"SWG" stringByAppendingString:discriminatedClassName]);

    if([self class ] == class) {
        return [super initWithDictionary:dict error:err];
    }


    return [[class alloc] initWithDictionary:dict error: err];

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top