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?
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] } ] }
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];
}
来源:https://stackoverflow.com/questions/22159573/jsonmodel-ios-and-polymorphism