This is a fork of EasyMapping, a flexible and easy framework for JSON mapping.
It turns out, that almost all popular libraries for JSON mapping are SLOW. The main reason for that is multiple trips to database during the lookup of existing objects. We decided to take an already existing flexible solution (i.e. EasyMapping) and improve its overall performance.
#Podfile
platform :ios, '7.0'
pod 'FastEasyMapping', '~> 1.1'
or add as a static library.
FEMMapping
<FEMProperty>
FEMAttribute
FEMRelationship
FEMDeserializer
FEMSerializer
FEMObjectStore
FEMManagedObjectStore
FEMManagedObjectCache
Today NSObject and NSManagedObject mapping are supported out of the box. Lets take a look at how a basic mapping looks like: For example, we have JSON:
{
"name": "Lucas",
"user_email": "lucastoc@gmail.com",
"car": {
"model": "i30",
"year": "2013"
},
"phones": [
{
"ddi": "55",
"ddd": "85",
"number": "1111-1111"
},
{
"ddi": "55",
"ddd": "11",
"number": "2222-222"
}
]
}
and corresponding CoreData-generated classes:
@interface Person : NSManagedObject
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSString *email;
@property (nonatomic, retain) Car *car;
@property (nonatomic, retain) NSSet *phones;
@end
@interface Car : NSManagedObject
@property (nonatomic, retain) NSString *model;
@property (nonatomic, retain) NSString *year;
@property (nonatomic, retain) Person *person;
@end
@interface Phone : NSManagedObject
@property (nonatomic, retain) NSString *ddi;
@property (nonatomic, retain) NSString *ddd;
@property (nonatomic, retain) NSString *number;
@property (nonatomic, retain) Person *person;
@end
In order to map JSON to Object and vice versa we have to describe the mapping rules:
@implementation Person (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Person"];
[mapping addAttributesFromArray:@[@"name"]];
[mapping addAttributesFromDictionary:@{@"email": @"user_email"}];
[mapping addRelationshipMapping:[Car defaultMapping] forProperty:@"car" keyPath:@"car"];
[mapping addToManyRelationshipMapping:[Phone defaultMapping] forProperty:@"phones" keyPath:@"phones"];
return mapping;
}
@end
@implementation Car (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Car"];
[mapping addAttributesFromArray:@[@"model", @"year"]];
return mapping;
}
@end
@implementation Phone (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Phone"];
[mapping addAttributesFromArray:@[@"number", @"ddd", @"ddi"]];
return mapping;
}
@end
Now we can deserialize JSON to Object easily:
FEMMapping *mapping = [Person defaultMapping];
Person *person = [FEMDeserializer objectFromRepresentation:json mapping:mapping context:managedObjectContext];
Or collection of Objects:
NSArray *persons = [FEMDeserializer collectionFromRepresentation:json mapping:mapping context:managedObjectContext];
Or even update an Object:
[FEMDeserializer fillObject:person fromRepresentation:json mapping:mapping];
Also we can serialize an Object to JSON using the mapping defined above:
FEMMapping *mapping = [Person defaultMapping];
Person *person = ...;
NSDictionary *json = [FEMSerializer serializeObject:person usingMapping:mapping];
Or collection to JSON:
FEMMapping *mapping = [Person defaultMapping];
NSArray *persons = ...;
NSArray *json = [FEMSerializer serializeCollection:persons usingMapping:mapping];
FEMAttribute
is a core class of FEM. Briefly it is a description of relationship between the Object's property
and the JSON's keyPath
. Also it encapsulates knowledge of how the value needs to be mapped from Object to JSON and back via blocks.
typedef __nullable id (^FEMMapBlock)(id value __nonnull);
@interface FEMAttribute : NSObject <FEMProperty>
@property (nonatomic, copy, nonnull) NSString *property;
@property (nonatomic, copy, nullable) NSString *keyPath;
- (nonnull instancetype)initWithProperty:(nonnull NSString *)property keyPath:(nullable NSString *)keyPath map:(nullable FEMMapBlock)map reverseMap:(nullable FEMMapBlock)reverseMap;
- (nullable id)mapValue:(nullable id)value;
- (nullable id)reverseMapValue:(nullable id)value;
@end
Alongside with property
and keyPath
value you can pass mapping blocks that allow to describe completely custom mappings.
Examples:
FEMAttribute *attribute = [FEMAttribute mappingOfProperty:@"url"];
// or
FEMAttribute *attribute = [[FEMAttribute alloc] initWithProperty:@"url" keyPath:@"url" map:NULL reverseMap:NULL];
FEMAttribute *attribute = [FEMAttribute mappingOfProperty:@"urlString" toKeyPath:@"URL"];
// or
FEMAttribute *attribute = [[FEMAttribute alloc] initWithProperty:@"urlString" keyPath:@"URL" map:NULL reverseMap:NULL];
Quite often value type in JSON needs to be converted to more useful internal representation. For example HEX to UIColor
, String
to NSURL
, Integer
to enum
and so on. For this purpose you can use map
and reverseMap
properties. For example lets describe attribute that maps String
to NSDate using NSDateFormatter:
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
[formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
FEMAttribute *attribute = [[FEMAttribute alloc] initWithProperty:@"updateDate" keyPath:@"timestamp" map:^id(id value) {
if ([value isKindOfClass:[NSString class]]) {
return [formatter dateFromString:value];
}
return nil;
} reverseMap:^id(id value) {
return [formatter stringFromDate:value];
}];
First of all we've defined NSDateFormatter that fits our requirements. Next step is to define Attribute instance with correct mapping. Briefly map
block is invoked during deserialization (JSON to Object) while reverseMap
is used for serialization process. Both are quite stratforward with but with few gotchas:
map
can receiveNSNull
instance. This is a valid case fornull
value in JSON.- map won't be invoked for missing keys. Therefore, if JSON doesn't contain keyPath specified by your attribute, reverse mapping not called.
- from map you can return either
nil
orNSNull
for empty values reverseMap
invoked only whenproperty
contains a non-nil value.- from
reverseMap
you can return eithernil
orNSNull
. Both will produce{"keyPath": null}
There are several shortcuts that allow you to add attributes easier to the mapping itself:
FEMMapping *mapping = [[FEMMapping alloc] initWithObjectClass:[Person class]];
FEMAttribute *attribute = [FEMAttribute mappingOfProperty:@"url"];
[mapping addAttribute:attribute];
FEMMapping *mapping = [[FEMMapping alloc] initWithObjectClass:[Person class]];
[mapping addAttributeWithProperty:@"property" keyPath:@"keyPath"];
FEMMapping *mapping = [[FEMMapping alloc] initWithObjectClass:[Person class]];
[mapping addAttributesFromDictionary:@{@"property": @"keyPath"}];
Useful when the property
is equal to the keyPath
:
FEMMapping *mapping = [[FEMMapping alloc] initWithObjectClass:[Person class]];
[mapping addAttributesFromArray:@[@"propertyAndKeyPathAreTheSame"]];
FEMRelationship
is a class that describes relationship between two FEMMapping
instances.
@interface FEMRelationship
@property (nonatomic, copy, nonnull) NSString *property;
@property (nonatomic, copy, nullable) NSString *keyPath;
@property (nonatomic, strong, nonnull) FEMMapping *mapping;
@property (nonatomic, getter=isToMany) BOOL toMany;
@property (nonatomic) BOOL weak;
@property (nonatomic, copy, nonnull) FEMAssignmentPolicy assignmentPolicy;
@end
Relationship is also bound to a property
and keyPath
. Obviously, it has a reference to Object's FEMMapping
and a flag that indicates whether it’s a to-many relationship. Moreover, it allows you to specify assignment policy and "weakifying" behaviour of the relationship.
Example:
FEMMapping *childMapping = ...;
FEMRelationship *childRelationship = [[FEMRelationship alloc] initWithProperty:@"parentProperty" keyPath:@"jsonKeyPath" mapping:childMapping];
childRelationship.toMany = YES;
Assignment policy describes how deserialized relationship value should be assigned to a property. FEM supports 5 policies out of the box:
FEMAssignmentPolicyAssign
- replace Old property's value by New. Designed for to-one and to-many relationship. Default policy.FEMAssignmentPolicyObjectMerge
- assigns New relationship value unless it isnil
. Designed for to-one relationship.FEMAssignmentPolicyCollectionMerge
- merges a New and Old values of relationship. Supported collections are: NSSet, NSArray, NSOrderedSet and their successors. Designed for to-many relationship.FEMAssignmentPolicyObjectReplace
- replaces Old value with New by deleting Old. Designed for to-one relationship.FEMAssignmentPolicyCollectionReplace
- deletes objects not presented in union of New and Old values sets. Union set is used as a New value. Supported collections are: NSSet, NSArray, NSOrderedSet and their successors. Designed for to-many relationship.
FEMMapping *mapping = [[FEMMapping alloc] initWithObjectClass:[Person class]];
FEMMapping *carMapping = [[FEMMapping alloc] initWithObjectClass:[Car class]];
FEMRelationship *carRelationship = [[FEMRelationship alloc] initWithProperty:@"car" keyPath:@"car" mapping:carMapping];
[mapping addRelationship:carRelationship];
FEMMapping *mapping = [[FEMMapping alloc] initWithObjectClass:[Person class]];
FEMMapping *phoneMapping = [[FEMMapping alloc] initWithObjectClass:[Phone class]];
[mapping addToManyRelationshipMapping:phoneMapping property:@"phones" keyPath:@"phones"];
Generally FEMMapping
is a class that describes mapping for NSObject
or NSManagedObject
by encapsulating a set of attributes and relationships. In addition, it defines the possibilities for objects uniquing (supported by CoreData only).
The only difference between NSObject
and NSManagedObject
is in init
methods:
FEMMapping *objectMapping = [[FEMMapping alloc] initWithObjectClass:[CustomNSObjectSuccessor class]];
FEMMapping *managedObjectMapping = [[FEMMapping alloc] initWithEntityName:@"EntityName"];
Sometimes a desired JSON is nested by a keyPath. In this case you can use rootPath
property. Let’s modify Person JSON by nesting Person representation:
{
result: {
"name": "Lucas",
"user_email": "lucastoc@gmail.com",
"car": {
"model": "i30",
"year": "2013"
}
}
}
Mapping will look like this:
@implementation Person (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Person"];
mapping.rootPath = @"result";
[mapping addAttributesFromArray:@[@"name"]];
[mapping addAttributesFromDictionary:@{@"email": @"user_email"}];
[mapping addRelationshipMapping:[Car defaultMapping] forProperty:@"car" keyPath:@"car"];
return mapping;
}
@end
IMPORTANT:
FEMMapping.rootPath
is ignore during relationship mapping. UseFEMRelationship.keyPath
instead!
It is a common case when you're deserializing JSON into CoreData and don't want to duplicate data in your database. This can be easily achieved by utilizing FEMMapping.primaryKey
. It informs FEMDeserializer
to track primary keys and avoid data copying. For example lets make Person's email
a primary key attribute:
@implementation Person (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Person"];
mapping.primaryKey = @"email";
[mapping addAttributesFromArray:@[@"name"]];
[mapping addAttributesFromDictionary:@{@"email": @"user_email"}];
[mapping addRelationshipMapping:[Car defaultMapping] forProperty:@"car" keyPath:@"car"];
return mapping;
}
@end
We recommend to index your primary key in datamodel to speedup keys lookup. Supported values for primary keys are Strings and Integers.
Starting from second import FEMDeserializer
will update existing Person
.
Sometimes object representation contains a relationship described by a PK of the target entity:
{
"result": {
"id": 314
"title": "https://github.com"
"category": 4
}
}
As you can see, from JSON we have two objects: Website
and Category
. If Website
can be imported easily, there is an external reference to a Category
represented by its primary key id
. Can we bind the Website
to the corresponding category? Yep! We just need to treat Website's representation as a Category:
First of all let’s declare our classes:
@interface Website: NSManagedObject
@property (nonatomic, strong) NSNumber *identifier;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) Category *category;
@end
@interface Category: NSManagedObject
@property (nonatomic, strong) NSNumber *identifier;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSSet *websites
@end
Now it is time to define mapping for Website
:
@implementation Website (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Website"];
mapping.primaryKey = @"identifier";
[mapping addAttributesFromDictionary:@{@"identifier": @"id", @"title": @"title"}];
FEMMapping *categoryMapping = [[FEMMapping alloc] initWithEntityName:@"Category"];
categoryMapping.primaryKey = @"identifier";
[categoryMapping addAttributesFromDictionary:@{@"identifier": @"category"}];
[mapping addRelationshipMapping:categoryMapping property:@"category" keyPath:nil];
return mapping;
}
@end
By specifying nil
as a keyPath
for the category Website
's representation is treated as a Category
at the same time. In this way it is easy to bind objects that are passed by PKs (which is quite common for network).
In the example above there is an issue: what if our database doesn't contain Category
with PK = 4
? By default FEMDeserializer
creates new objects during deserialization lazily. In our case this leads to insertion of Category
instance without any data except identifier
. In order to prevent such inconsistencies we can set FEMRelationship.weak
to YES
:
@implementation Website (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Website"];
mapping.primaryKey = @"identifier";
[mapping addAttributesFromDictionary:@{@"identifier": @"id", @"title": @"title"}];
FEMMapping *categoryMapping = [[FEMMapping alloc] initWithEntityName:@"Category"];
categoryMapping.primaryKey = @"identifier";
[categoryMapping addAttributeWithProperty:@"identifier" keyPath:nil];
FEMRelationship *categoryRelationship = [[FEMRelationship alloc] initWithProperty:@"category" keyPath:@"category" mapping:categoryMapping];
categoryRelationship.weak = YES;
[mapping addRelationship:categoryRelationship];
return mapping;
}
@end
As a result it'll bind the Website
with the corresponding Category
only if the latter exists.
You can customize deserialization process by implementing FEMDeserializerDelegate
protocol:
@protocol FEMDeserializerDelegate <NSObject>
@optional
- (void)deserializer:(nonnull FEMDeserializer *)deserializer willMapObjectFromRepresentation:(nonnull id)representation mapping:(nonnull FEMMapping *)mapping;
- (void)deserializer:(nonnull FEMDeserializer *)deserializer didMapObject:(nonnull id)object fromRepresentation:(nonnull id)representation mapping:(nonnull FEMMapping *)mapping;
- (void)deserializer:(nonnull FEMDeserializer *)deserializer willMapCollectionFromRepresentation:(nonnull NSArray *)representation mapping:(nonnull FEMMapping *)mapping;
- (void)deserializer:(nonnull FEMDeserializer *)deserializer didMapCollection:(nonnull NSArray *)collection fromRepresentation:(nonnull NSArray *)representation mapping:(nonnull FEMMapping *)mapping;
@end
However, if you're using Delegate you also have to instantiate FEMDeserializer
manually:
FEMDeserializer *deserializer = [[FEMDeserializer alloc] init];
deserializer.delegate = self;
FEMDeserializer *deserializer = [[FEMDeserializer alloc] initWithContext:managedObjectContext];
deserializer.delegate = self;
Note, that delegate methods will be called on every object and collection during deserialization. Lets use Person
example:
{
"name": "Lucas",
"user_email": "lucastoc@gmail.com",
"phones": [
{
"ddi": "55",
"ddd": "85",
"number": "1111-1111"
}
]
}
Mapping:
@implementation Person (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Person"];
[mapping addAttributesFromArray:@[@"name"]];
[mapping addAttributesFromDictionary:@{@"email": @"user_email"}];
[mapping addToManyRelationshipMapping:[Person defaultMapping] forProperty:@"phones" keyPath:@"phones"];
return mapping;
}
@end
@implementation Phone (Mapping)
+ (FEMMapping *)defaultMapping {
FEMMapping *mapping = [[FEMMapping alloc] initWithEntityName:@"Phone"];
[mapping addAttributesFromArray:@[@"number", @"ddd", @"ddi"]];
return mapping;
}
@end
During deserialization of persons collection order will be the following:
- willMapCollectionFromRepresentation:
Persons Array
mapping:Person mapping
- willMapObjectFromRepresentation:
Person Dictionary
mapping:Person mapping
- willMapCollectionFromRepresentation:
Phones Array
mapping:Phone mapping
- willMapObjectFromRepresentation:
Phone Dictionary
mapping:Phone mapping
- didMapObject:
Phone instance
fromRepresentation:Phone Dictionary
mapping:Phone mapping
- didMapObject:
Person instance
fromRepresentation:Person Dictionary
mapping:Person mapping
- didMapCollection:
Persons instances Array
fromRepresentation:Persons Array
mapping:Person mapping
Moved to releases
- Special thanks to lucasmedeirosleite for amazing framework.
Read out blogpost about FastEasyMapping.