Mapping relationships from JSON array with Restkit - ios

I will try to be as descriptive as possible with this issue...
Scenario
Let's say i have a NSManagedObject 'User'
#class Premise;
#interface User : NSManagedObject
#property (nonatomic, retain) NSNumber * identifier;
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSSet *premises;
#end
#interface User (CoreDataGeneratedAccessors)
- (void)addPremisesObject:(Premise *)value;
- (void)removePremisesObject:(Premise *)value;
- (void)addPremises:(NSSet *)values;
- (void)removePremises:(NSSet *)values;
#end
And i also have a NSManagedObject 'Premise'
#class User;
#interface Premise : NSManagedObject
#property (nonatomic, retain) NSNumber * identifier;
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) User *user;
#end
Based on it, i am creating a relationship route to map a JSON array of 'Premise' to the 'premises' attribute on the 'User' object.
Here's the route:
let getPremisesRoute = RKRoute(relationshipName: "premises",
objectClass: User.self,
pathPattern: "user/:identifier/premises",
method: .GET)
Here's the JSON response (/user/1/premises):
[
{
"id": 35,
"name": "Icaraí"
},
{
"id": 32,
"name": "Remanso"
}
]
Here's the response descriptor:
let getPremisesResponseDescriptor = RKResponseDescriptor(mapping: premiseMapping, method: .GET, pathPattern: "user/:identifier/premises", keyPath: nil, statusCodes: RKStatusCodeIndexSetForClass(.Successful))
And here are the respective mappings of 'User' and 'Premise'
let userMapping = RKEntityMapping(forEntityForName: "User", inManagedObjectStore: moc)
userMapping.addAttributeMappingsFromDictionary(["id":"identifier", "name":"name"])
userMapping.identificationAttributes = ["identifier"]
userMapping.addPropertyMapping(RKRelationshipMapping(fromKeyPath: nil, toKeyPath: "premises", withMapping: premiseMapping))
let premiseMapping = RKEntityMapping(forEntityForName: "Premise", inManagedObjectStore: moc)
premiseMapping.addAttributeMappingsFromDictionary(["id":"identifier", "name":"name"])
premiseMapping.identificationAttributes = ["identifier"]
Now to my problem
Apparently, Restkit is getting a little bit confused during the mapping process. Here's the database after the request:
User Table:
Premise Table:
Note the the relationship is not being created between the entities.
Now, if I change the response descriptor's mapping from premise to user mapping, the database changes to this:
Users Table:
Premise Table:
I'm really confused on what's going on and I've tried a lot of solutions with no success.
Is the JSON response out of pattern or am I doing something wrong? The JSON response seems to be on a common pattern, with a nil key path.

You're approaching the mapping incorrectly, or at least your mappings are wrong for what you're doing. Consider that the response is a user, but only the premises for a user, instead of considering it as a simple list of premises as you are now. Then you map to a user and insert the premises. Something like:
RKResponseDescriptor(mapping: userMapping, method: .GET, pathPattern: "user/:identifier/premises", keyPath: nil, statusCodes: RKStatusCodeIndexSetForClass(.Successful))
And here are the respective mappings of 'User' and 'Premise'
let userMapping = RKEntityMapping(forEntityForName: "User", inManagedObjectStore: moc)
userMapping.addAttributeMappingsFromDictionary(["#metadata.routing.parameters.idEntities":"identifier"])
userMapping.identificationAttributes = ["identifier"]
userMapping.addPropertyMapping(RKRelationshipMapping(fromKeyPath: nil, toKeyPath: "premises", withMapping: premiseMapping))
let premiseMapping = RKEntityMapping(forEntityForName: "Premise", inManagedObjectStore: moc)
premiseMapping.addAttributeMappingsFromDictionary(["id":"identifier", "name":"name"])
premiseMapping.identificationAttributes = ["identifier"]
You don't have a user name in the response so you can't map it, and the user id is actually in the request so you need to use metadata to extract it.

Ok, I found a possible solution based on #Wain's solution using foreign keys.
I added a new property 'userID' to the 'Premise' entity and mapped it to the identifier on the URL using metadata
let premiseMapping = RKEntityMapping(forEntityForName: "Premise", inManagedObjectStore: moc)
premiseMapping.addAttributeMappingsFromDictionary(["id":"identifier", "name":"name", "#metadata.routing.parameters.identifier":"userID"])
Then I added a relationship connection to the 'premiseMapping'
premiseMapping.addConnectionForRelationship("user", connectedBy: ["userID":"identifier"])
If anyone has a more elegant solution please share with us.

Related

Restkit fails to use appropriate Mapping

I am using Restkit 0.26.0 to map JSON with multiple layers and this key path causes trouble:
productColorImages: [
{
id: 10,
productId: "232",
color: "green",
url: "exampleURL.com"
},
{
id: 11,
productId: "232",
color: "red",
url: "exampleURL.com"
},
{
id: 12,
productId: "232",
color: "blue",
url: "exampleURL.com"
}
],
My mapping
RKObjectMapping *transferProductColorImageMapping = [RKObjectMapping mappingForClass:[TransferProductColorImage class]];
[transferProductColorImageMapping addAttributeMappingsFromDictionary:#{ #"identifier" : #"id",
#"URL" : #"URL",
#"productId" : #"productId",
#"color" : #"color"
[getProductPageMapping addRelationshipMappingWithSourceKeyPath:#"productColorImages" mapping:transferProductColorImageMapping];
The destination class
#interface TransferProductColorImage : NSObject
#property (nonatomic) NSNumber * identifier; // Mapps to id ;int 64
#property (nonatomic) NSString * URL;
#property (nonatomic) NSString * productId;
#property (nonatomic) NSString * color;
The problem is that the resulting Object is of the right class but the properties are not filled.
I searched in the logs and found this:
2016-05-30 17:42:24.007 thebrandsapp[20697:7047455] T restkit.object_mapping:RKMappingOperation.m:1173 Performing mapping operation: <RKMappingOperation 0x7f9e3b9c11e0> for 'TransferProductColorImage' object. Mapping values from object {
color = green;
id = 10;
productId = 232;
url = "exampleURL.com";
} to object <TransferProductColorImage: 0x7f9e3b846d60> with object mapping (null)
On the Model Object the productColorImages is a NSArray:
#property (nonatomic) NSArray <TransferProductColorImage *> * productColorImages;
This is odd because. How does Restkit know what mapping to use if the mapping is null. Is there any reason why the object mapping could be set to null?
Update: I found that the identifier and the color property were mapped correctly it is, the identifier and the url are not mapped correctly.
I changed the URL property to be lowercase: "url" and mapped it exactly like I did before and it worked. It seems like the the all-caps property name threw RestKit of.
I swooped identifier and id, now they map correctly as well.

How to create model classes using JSONModel?

I am trying to create Model Class by using JSONModel.My json Dictionary after using NSJSONSerialization looks like below.
{
apiStatus = {
message = SUCCESS;
success = 1;
};
boardingPoints = "<null>";
inventoryType = 0;
seats = (
{
ac = 0;
available = 1;
bookedBy = "<null>";
commission = "<null>";
fare = 1200;
},
{
ac = 0;
available = 1;
bookedBy = "<null>";
commission = "<null>";
fare = 1200;
},
);
}
The JSON looks like this:
{"boardingPoints":null,"inventoryType":0,"apiStatus":{"success":true,"message":"‌​SUCCESS"},"seats":[{"fare":1200,"commission":null,"bookedBy":null,"ac":false,"ava‌​ilable":true},{"fare":1200,"commission":null,"bookedBy":null,"ac":false,"availabl‌​e":true},]}
I have a model class like this :-
#interface Seat : JSONModel
#property (nonatomic,strong)NSString *ac;
#property (nonatomic,strong)NSString *available;
#property(nonatomic,strong)NSString *bookedBy;
#property(nonatomic,strong)NSString *comission;
#property(nonatomic)NSNumber * fare;
For mapping keys I have done like this:-
+(JSONKeyMapper*)keyMapper {
return [[JSONKeyMapper alloc] initWithDictionary:#{#"ac":#"ac",
#"available":#"available",
#"bookedBy":#"bookedBy",
#"commission":#"commission",
#"fare":#"fare", }];
}
However when I try to use this model I get the following error:
[JSONModel.m:252] Incoming data was invalid [Seat initWithDictionary:]. Keys missing: {(
bookedBy,
comission,
available,
ac,
fare,
)}
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[0]'
I am using it like this:
//Using JSON Model.
NSError *jsonError;
seat = [[Seat alloc]initWithDictionary:jsonDictionary error:&jsonError];
jsonArray = #[seat.ac,seat.available,seat.bookedBy,seat.comission,seat.fare];
NSLog(#"JSON Model Array : %#", jsonArray);
How to use it correctly?
First of all, you don't need to override +(JSONKeyMapper*)keyMapper if your property names matches with the field names. Try putting the optional keyword for fields that can be null.
#interface Seat : JSONModel
#protocol Seat;
#property (nonatomic,strong)NSString *ac;
#property (nonatomic,strong)NSString<Optional> *available;
#property(nonatomic,strong)NSString<Optional> *bookedBy;
#property(nonatomic,strong)NSString *comission;
#property(nonatomic)NSNumber * fare;
#end
Taking this a step further, you can do cascading in your top class like this:
//Result.h
#import "Seat.h"
#import "APIStatus.h"
#interface Result: JSONModel
#property(nonatomic,strong) APIStatus *apiStatus;
#property(nonatomic,strong) NSString<Optional> *boardingPoints;
#property(nonatomic,strong) NSArray<Seat> *seats;
#property(nonatomic,strong) NSNumber *boardingPoints;
#end
//APIStatus.h
#interface APIStatus: JSONModel
#property(nonatomic,strong) NSString *message;
#property(nonatomic,strong) NSNumber *success;
#end
EDIT: This way you only need to init the Result model with JSONModel, all the intermediary classes will be created automatically. You may need to play around with the property types. JSONModel's github page offers good amount of explanations if you need a reference.
You need to correct your JSON string - remove the comma after the last entry of seats.
I think you may have to indicate you are working with nested models here i.e. seats are a nested model of your top level model which at this point is anonymous but really denoted by the top level { } braces.
Have a look at this - it describes how to set things up with nested models.
JSONModel tutorial covering nested models
Here is your corrected JSON string:
{
"boardingPoints": null,
"inventoryType": 0,
"apiStatus": {
"success": true,
"message": "‌​SUCCESS"
},
"seats": [
{
"fare": 1200,
"commission": null,
"bookedBy": null,
"ac": false,
"ava‌​ilable": true
},
{
"fare": 1200,
"commission": null,
"bookedBy": null,
"ac": false,
"availabl‌​e": true
}
]}

RKObjectMapping connections

I have neхt response:
{
"orders": [
{
"date": "2013-11-18T13:00:39.000Z",
"state": "new",
"id": 11,
"order_item_ids": [
27,
28
]
}
],
"order_items": [
{
"count": 2,
"id": 27,
"item_id": 1,
"order_id": 11
},
{
"count": 1,
"id": 28,
"item_id": 2,
"order_id": 11
}
]
}
And next object I want to map it:
#interface Order : NSObject
#property (nonatomic, strong) NSString* state;
#property (nonatomic, strong) NSMutableArray* orderItems;
#property (nonatomic, strong) NSDate* visitDate;
#end
orderItems should be array of:
#interface OrderItem : NSObject
#property (nonatomic) NSUInteger ID;
#property (nonatomic) NSUInteger itemID;
#property (nonatomic) NSUInteger count;
#end
In case of RKEntityMapping I should use addConnectionForRelationship to map order_item_ids to orderItems array via order_items response. But what should I do to do to connect it in case of RKObjectMapping? Off course I can map both orders and orderItems with separate response descriptors and than parse it, but I want to make RestKit do it for me. Another idea is to use CoreData and RKEntityMapping but I'm not sure that I want in this case, it will be overkill in this case.
You can't have it done automatically with RKObjectMapping because you can't index into both arrays in any way. The entity mapping only works because you can use foreign key mapping after both mappings have been performed. You can duplicate this by using multiple response descriptors and mappings and then combining the contents of the mapping result.

JSONModel: json to array?

Is it possible to parse json to an array of JSONModel objects using JSONModel? Like parse json below:
[{"id" : 1}, {"id" : 2}]
to two JSONModel objects having property of "id".
You should use arrayOfModelsFromDictionaries: on your model class like so:
NSArray* models = [YourModelClass arrayOfModelsFromDictionaries: objects];
This will go over "objects" and convert each dictionary to a model and you get the result in models. If any item in objects fail to convert to model "models" will be nil.
Here's the class docs for this method:
http://jsonmodel.com/docs/Classes/JSONModel.html#//api/name/arrayOfModelsFromDictionaries:
Why not trying BWJSONMatcher?
First you should declare your own data model:
#interface MyDataModel : NSObject
#property (nonatomic, assign) NSInteger id;
#end
Then you can easily get your array with BWJSONMatcher within one line:
NSArray *jsonArray = [BWJSONMatcher matchJSON:jsonString withClass:[MyDataModel class]];
Hope this could help you.

Did not find mappable relationship value keyPath with RestKit

I'm unable to map my JSON to Core Data with RestKit. RK logging is saying:
Did not find mappable relationship value keyPath.
I'm clearly doing something wrong.
Log:
2012-11-20 15:40:36.525 eMobile[44849:12603] T restkit.object_mapping:RKObjectMappingOperation.m:342 Mapping attribute value keyPath 'OBJECTCLASS' to 'type'
2012-11-20 15:40:36.525 eMobile[44849:12603] T restkit.object_mapping:RKObjectMappingOperation.m:359 Skipped mapping of attribute value from keyPath 'OBJECTCLASS to keyPath 'type' -- value is unchanged (SFAACCOUNT)
2012-11-20 15:40:36.525 eMobile[44849:12603] T restkit.object_mapping:RKObjectMappingOperation.m:342 Mapping attribute value keyPath 'TIMESTAMP' to 'timestamp'
2012-11-20 15:40:36.526 eMobile[44849:12603] T restkit.object_mapping:RKObjectMappingOperation.m:352 Mapped attribute value from keyPath 'TIMESTAMP' to 'timestamp'. Value: 2012-11-20T15:40:08Z
2012-11-20 15:40:36.526 eMobile[44849:12603] D restkit.object_mapping:RKObjectMappingOperation.m:483 Did not find mappable relationship value keyPath 'VALUES'
2012-11-20 15:40:36.526 eMobile[44849:12603] D restkit.object_mapping:RKObjectMappingOperation.m:662 Finished mapping operation successfully...
2012-11-20 15:40:36.526 eMobile[44849:12603] T restkit.object_mapping:RKObjectMapper.m:293 Examining keyPath 'VALUES' for mappable content...
2012-11-20 15:40:36.527 eMobile[44849:12603] D restkit.object_mapping:RKObjectMapper.m:303 Found unmappable value at keyPath: VALUES
2012-11-20 15:40:36.527 eMobile[44849:12603] D restkit.object_mapping:RKObjectMapper.m:367 The following operations are in the queue: (
)
2012-11-20 15:40:36.527 eMobile[44849:12603] D restkit.object_mapping:RKObjectMapper.m:382 Finished performing object mapping. Results: {
JSONDATA = (
"<EntityClass: 0x127da9e0> (entity: EntityClass; id: 0x127527d0 <x-coredata://E20EA003-1D5A-4B27-92DF-DFD8BE7074CB/EntityClass/p2> ; data: {\n timestamp = \"2012-11-20T15:40:08Z\";\n type = VCFUND;\n values = \"<relationship fault: 0x12703200 'values'>\";\n})",
"<EntityClass: 0x127da6b0> (entity: EntityClass; id: 0x12695dc0 <x-coredata://E20EA003-1D5A-4B27-92DF-DFD8BE7074CB/EntityClass/p1> ; data: {\n timestamp = \"2012-11-20T15:40:08Z\";\n type = SFAACCOUNT;\n values = \"<relationship fault: 0x11efb910 'values'>\";\n})"
);
}
2012-11-20 15:40:36.530 eMobile[44849:11603] (
"<EntityClass: 0x126b7400> (entity: EntityClass; id: 0x127527d0 <x-coredata://E20EA003-1D5A-4B27-92DF-DFD8BE7074CB/EntityClass/p2> ; data: <fault>)",
"<EntityClass: 0x127de030> (entity: EntityClass; id: 0x12695dc0 <x-coredata://E20EA003-1D5A-4B27-92DF-DFD8BE7074CB/EntityClass/p1> ; data: <fault>)"
)
JSON sample:
{
"JSONDATA": [
{
"OBJECTCLASS": "VCFUND",
"DESCRIPTION": {
"VALUES": [
{
"DATA": [
{
"NAME": "Fund 1",
"VALUE": "Buyout Fund 1"
},
{
"NAME": "Fund 2",
"VALUE": "Buyout Fund 2"
},
{
"NAME": "Fund 3",
"VALUE": "Buyout Fund 3"
}
],
"IQID": "059386B4D26249358C68E978D3C10C84"
}
]
},
"TIMESTAMP": "2012-11-17T22:03:55Z"
},
{
"OBJECTCLASS": "PROPERTIES",
"DESCRIPTION": {
"VALUES": [
{
"DATA": [
{
"NAME": "Property 1",
"VALUE": "Buyout Property 1"
},
{
"NAME": "Property 2",
"VALUE": "Buyout Property 2"
},
{
"NAME": "Property 3",
"VALUE": "Buyout Property 3"
}
],
"IQID": "456789087654678909876589098"
}
]
},
"TIMESTAMP": "2012-11-17T22:03:55Z"
}
]
}
Here is my mapping:
objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:#"AppCD.sqlite"];
RKManagedObjectMapping* entityClassItemValueMapping = [RKManagedObjectMapping mappingForClass:[EntityClassItemValue class] inManagedObjectStore:[[RKObjectManager sharedManager] objectStore]];
entityClassItemValueMapping.primaryKeyAttribute = #"name";
[entityClassItemValueMapping mapKeyPath:#"NAME" toAttribute:#"name"];
[entityClassItemValueMapping mapKeyPath:#"VALUE" toAttribute:#"value"];
RKManagedObjectMapping* entityClassItemMapping = [RKManagedObjectMapping mappingForClass:[EntityClassItem class] inManagedObjectStore:[[RKObjectManager sharedManager] objectStore]];
entityClassItemMapping.primaryKeyAttribute = #"iqid";
[entityClassItemMapping mapKeyPath:#"IQID" toAttribute:#"iqid"];
RKManagedObjectMapping* entityClassMapping = [RKManagedObjectMapping mappingForClass:[EntityClass class] inManagedObjectStore:[[RKObjectManager sharedManager] objectStore]];
entityClassMapping.primaryKeyAttribute = #"type";
[entityClassMapping mapKeyPath:#"OBJECTCLASS" toAttribute:#"type"];
[entityClassMapping mapKeyPath:#"TIMESTAMP" toAttribute:#"timestamp"];
[entityClassMapping mapKeyPath:#"VALUES" toRelationship:#"values" withMapping:entityClassItemMapping];
[entityClassItemMapping mapKeyPath:#"DATA" toRelationship:#"entityClassItemValues" withMapping:entityClassItemValueMapping];
[[RKObjectManager sharedManager].mappingProvider setMapping:entityClassMapping forKeyPath:#"JSONDATA"];
[[RKObjectManager sharedManager].mappingProvider setMapping:entityClassItemMapping forKeyPath:#"VALUES"];
[[RKObjectManager sharedManager].mappingProvider setMapping:entityClassItemValueMapping forKeyPath:#"DATA"];
[[objectManager client] setValue:[cookies objectForKey:#"Cookie"] forHTTPHeaderField:#"Cookie"];
NSDictionary *params = [[NSDictionary alloc] initWithObjectsAndKeys:query, #"Targ", #"", #"Query", #"F285E67F2C8CEC9837B68", #"$SESSION", #"3A7037FA806156C4", #"VERSION", nil];
[objectManager loadObjectsAtResourcePath:URL_DATA usingBlock:^(RKObjectLoader *loader) {
loader.method = RKRequestMethodPOST;
loader.params = params;
loader.mappingProvider = [RKObjectManager sharedManager].mappingProvider;
loader.onDidLoadObjects = loadBlock;
loader.onDidFailWithError = failBlock;
}];
My CoreData models are:
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#class EntityClassItem;
#interface EntityClass : NSManagedObject
#property (nonatomic, retain) NSString * timestamp;
#property (nonatomic, retain) NSString * type;
#property (nonatomic, retain) NSSet *values;
#end
#interface EntityClass (CoreDataGeneratedAccessors)
- (void)addValuesObject:(EntityClassItem *)value;
- (void)removeValuesObject:(EntityClassItem *)value;
- (void)addValues:(NSSet *)values;
- (void)removeValues:(NSSet *)values;
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#class EntityClassItem;
#interface EntityClass : NSManagedObject
#property (nonatomic, retain) NSString * timestamp;
#property (nonatomic, retain) NSString * type;
#property (nonatomic, retain) NSSet *values;
#end
#interface EntityClass (CoreDataGeneratedAccessors)
- (void)addValuesObject:(EntityClassItem *)value;
- (void)removeValuesObject:(EntityClassItem *)value;
- (void)addValues:(NSSet *)values;
- (void)removeValues:(NSSet *)values;
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#class EntityClassItem;
#interface EntityClassItemValue : NSManagedObject
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSString * value;
#property (nonatomic, retain) EntityClassItem *entityClassItem;
I figured it out after a lot of debugging and re-reading of the docs. It was a lack of understanding on my part around:
mapKeyPath:#"foo" toRelationship:#"bar" withMapping:fooBar];
As the docs clearly state, mapKeyPath must match both the property of the object you are mapping to in addition to the node in the JSON payload. My payload's case didn't match so it was failing.

Resources