-
Notifications
You must be signed in to change notification settings - Fork 0
Deserialization into Instances
In addition to creating new instances as part of the deserialization process, ClassMapper can be used to update an existing instance. This is semantically much more complex than creating a fresh instance, but can be useful. That being said, if you don't need it, you will probably be happier without it.
Let's say that you have a simple user class for a chat room application:
@interface User : NSObject
@property(nonatomic, strong)NSString *name;
@property(nonatomic, strong)NSString *status;
@endGiven an instance of User, you could update it with new data from some external service. Here's a quick example:
// Existing instance
User *elvis = [User new];
elvis.name = @"The King";
elvis.status = @"In the building";
// New Data
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:@"The King", @"name",
@"Left the building", @"status", nil];
// Update instance
[ClassMapper deserialize:dict toInstance:elvis];
elvis.status; // @"Left the building"There are a few non-obvious points here:
- The deserialization actually mutated the elvis instance. It also returns the object, but if you don't want elvis mutated, you should make a deep copy of elvis first.
- Even though the elvis.name did not change, the property will be set again as part of the deserialization. Any observers should be prepared to handle this.
- Only the properties specified by the keys in dict will be updated. If there are additional properties in the instance but not the dictionary, they will not be modified by deserialization.
Objects can still be created if they are needed. Examples only involving strings are not particularly illuminating here, as strings are always copied. Let's look at something more interesting:
@interface Location : NSObject
@property(nonatomic, strong)NSNumber *lat;
@property(nonatomic, strong)NSNumber *long;
@end
@interface Place : NSObject
@property(nonatomic, strong)NSString *name;
@property(nonatomic, strong)Location *location;
@end
// Existing instance
Place *place = [Place new];
place.name = @"The Pig";
// New Data
NSDictionary *locDict = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:70.0], @"lat",
[NSNumber numberWithFloat:70.0], @"long",
nil];
// Equivalent to {"name":"The Pig", "location":{"lat":70.0, "long":70.0}}
NSDictionary *placeDict = [NSDictionary dictionaryWithObjectsAndKeys:@"The Pig", @"name",
locDict, @"location", nil];
// Update instance
[ClassMapper deserialize:placeDict toInstance:place];
place.location.lat; // 70.0Since place didn't have an existing instance in place.location, one was created at the time of deserialization. If place.location already existed, then that instance would have had it's lat and long properties reset.
Deserializing into an instance of a collection is, if not ambiguous semantically, at least complex to reason about. On some level, we might want to always create a sync relationship between the collections. On the other hand, we might want the deserialization process to add the new elements into the collection (this corresponds to the semantic we have chosen for KVC objects). A third possibility is that we might not want to mutate the collection at all, but merely update the existing instances inside the collection.
Compounding these existing possibilities are the existence of both mutable and non-mutable collections inside of Cocoa (e.g. NSMutableArray and NSArray, respectively). We cannot add elements to or remove elements from non-mutable collections, so we can only choose one possible semantic: we can update the instances inside of the collection. Let's look at an example:
@interface Album : NSObject
@property(nonatomic, strong)NSString *name;
@property(nonatomic, strong)NSString *artist;
@end
@interface User : NSObject
@property(nonatomic, strong)NSString *name;
@property(nonatomic, strong)NSArray *topThree;
@end
// Existing instance
User *user = [User new];
user.name = @"Pat";
Album *one = [Album new];
one.name = @"Master of Reality";
one.artist = @"Black Sabbath";
...
Album *three = [Album new];
three.name = @"Pink Flag";
three.artist = @"Wire";
user.topThree = [NSArray arrayWithObjects:one, two, three];
// Discover new music - New Data
NSArray *cerealRay = [NSArray arrayWithObjects:
[NSDictionary dictionaryWithObjectsAndKeys:@"A Love Supreme",
@"name", @"John Coltrane", @"artist", nil],
[NSDictionary dictionaryWithObjectsAndKeys:@"Aqualung", @"name",
@"Jethro Tull", @"artist", nil],
[NSDictionary dictionaryWithObjectsAndKeys:@"I See A Darkness",
@"name", @"Will Oldham", @"artist", nil],
nil];
NSDictionary *userDict = [NSDictionary dictionaryWithObjectsAndKeys:@"Pat", @"name",
cerealRay, @"topThree", nil];
// Update instance
[ClassMapper deserialize:userDict toInstance:user];
Album *album = [user.topThree objectAtindex:0];
album.name; // @"A Love Supreme"So, what actually happened here is that for each instance of album in user.topThree, we mutated the instance according to the properties in that position in the input array. The array itself was not modified or replaced during this process. The objects in the serialized data were mapped to objects in the instance based on position. In the case where the instance contains and NSDictionary instead of an NSArray, like in the example, that matching will be done based on keys.
In some cases, like the one presented above the mutability of the collection doesn't matter. When deserializing into existing instances of NSMutableArray/NSMutableDictionary, all existing data will be removed and then the collection will be refilled with the new data.
Some users may wish that instead of wiping the collection first, deserializing into an existing collection would have an additive effect. This is rather easy to construct yourself by serializing into the Class and then copying the new data. The current semantic is very hard to re-create if you only have "additive deserialization," so it was chosen as the default.