Right way to serialize nested collection with RestKit - ios

I'm using RestKit to POST my Album class with his nested collection o songs (Song class)
I cannot find the right way to serialize the NSArray containing album songs: it serialize the whole collection into a kind of collection of NSDictionary.
I'm using v 0.10.0 and using NSObjects not CoreData
Here is my json structure:
{"album":
{"id":12,
"source":"apple",
"collection_name":"The Platinum Collection",
"artist_name":"Queen",
"collection_id":"28007467",
"genre_id":5,
"thumb":"mythumburl.jpg",
"creation_user_id":1,
"songs":[
{"id":16,
"track_name":"Bohemian Rhapsody",
"track_time_millis":353933,
"disc_number":1,
"track_number":1,
"track_time":"00:05:53"},
...
]
}
This is my actual mapping:
RKObjectMapping *songMapping = [RKObjectMapping mappingForClass:[Song class]];
[songMapping mapKeyPath:#"id" toAttribute:#"code"];
[songMapping mapKeyPath:#"source" toAttribute:#"source"];
[songMapping mapKeyPath:#"album_id" toAttribute:#"album_id"];
[songMapping mapKeyPath:#"track_name" toAttribute:#"track_name"];
[songMapping mapKeyPath:#"track_time_millis" toAttribute:#"track_time_millis"];
[songMapping mapKeyPath:#"track_time" toAttribute:#"track_time"];
[songMapping mapKeyPath:#"disc_number" toAttribute:#"disc_number"];
[songMapping mapKeyPath:#"track_number" toAttribute:#"track_number"];
[songMapping mapKeyPath:#"preview_url" toAttribute:#"preview_url"];
[objectManager.mappingProvider setObjectMapping:songMapping forKeyPath:#"song"];
[objectManager.mappingProvider setSerializationMapping:[songMapping inverseMapping] forClass:[Song class]];
RKObjectMapping *albumMapping = [RKObjectMapping mappingForClass:[Album class]];
...
[albumMapping hasMany:#"songs" withMapping:songMapping];
// Setup our object mappings
[objectManager.mappingProvider setObjectMapping:albumMapping forKeyPath:#"album"];
[objectManager.mappingProvider setSerializationMapping:[albumMapping inverseMapping] forClass:[Album class]];

Related

RestKit Add Property Mapping and Relationship Mapping

Please guide me about following problem.
I have two entities with relationship as shown following image
I am using latest version of RestKit with iOS 7
Now in my appDelegate i am using following mapping for "List" Entity
NSDictionary *listObjectMapping = #{
#"listID" : #"listID",
#"listName" : #"listName",
#"listSyncStatus" : #"listSyncStatus"
};
RKEntityMapping *listEntityMapping = [RKEntityMapping mappingForEntityForName:#"List" inManagedObjectStore:managedObjectStore];
[listEntityMapping addAttributeMappingsFromDictionary:listObjectMapping];
listEntityMapping.identificationAttributes = #[ #"listID" ];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:listEntityMapping
method:RKRequestMethodGET
pathPattern:#"/api/lists"
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptor:responseDescriptor];
//Inverse mapping, to perform a POST
RKRequestDescriptor *requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[listEntityMapping inverseMapping]
objectClass:[List class]
rootKeyPath:nil
method:RKRequestMethodPOST];
objectManager.requestSerializationMIMEType = RKMIMETypeJSON;
[objectManager setAcceptHeaderWithMIMEType:RKMIMETypeJSON];
[RKMIMETypeSerialization registerClass:[RKNSJSONSerialization class] forMIMEType:#"application/json"];
[objectManager addRequestDescriptor:requestDescriptor];
//Inverse mapping, to perform a PUT
requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[listEntityMapping inverseMapping]
objectClass:[List class]
rootKeyPath:nil
method:RKRequestMethodPUT];
objectManager.requestSerializationMIMEType = RKMIMETypeJSON;
[objectManager setAcceptHeaderWithMIMEType:RKMIMETypeJSON];
[RKMIMETypeSerialization registerClass:[RKNSJSONSerialization class] forMIMEType:#"application/json"];
[objectManager addRequestDescriptor:requestDescriptor];
and using following mapping for my Task object
NSDictionary *taskObjectMapping = #{
#"listID" : #"listID",
#"taskID" : #"taskID",
#"taskName" : #"taskName",
#"taskCompletionStatus" : #"taskCompletionStatus",
#"taskSyncStatus" : #"taskSyncStatus"
};
RKEntityMapping *taskEntityMapping = [RKEntityMapping mappingForEntityForName:#"Task" inManagedObjectStore:managedObjectStore];
[taskEntityMapping addAttributeMappingsFromDictionary:taskObjectMapping];
taskEntityMapping.identificationAttributes = #[ #"taskID" ];
RKResponseDescriptor *taskResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:taskEntityMapping
method:RKRequestMethodGET
pathPattern:#"/api/list/:id"
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptor:taskResponseDescriptor];
//Inverse mapping, to perform a POST
RKRequestDescriptor *taskRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[taskEntityMapping inverseMapping]
objectClass:[Task class]
rootKeyPath:nil
method:RKRequestMethodPOST];
objectManager.requestSerializationMIMEType = RKMIMETypeJSON;
[objectManager setAcceptHeaderWithMIMEType:RKMIMETypeJSON];
[RKMIMETypeSerialization registerClass:[RKNSJSONSerialization class] forMIMEType:#"application/json"];
[objectManager addRequestDescriptor:taskRequestDescriptor];
//Inverse mapping, to perform a PUT
taskRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[taskEntityMapping inverseMapping]
objectClass:[Task class]
rootKeyPath:nil
method:RKRequestMethodPUT];
objectManager.requestSerializationMIMEType = RKMIMETypeJSON;
[objectManager setAcceptHeaderWithMIMEType:RKMIMETypeJSON];
[RKMIMETypeSerialization registerClass:[RKNSJSONSerialization class] forMIMEType:#"application/json"];
[objectManager addRequestDescriptor:taskRequestDescriptor];
Now my question is how to add relationship mapping between these two entites ?
What would be proper way ?
If i use i use following line of code
[taskEntityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"list.listID"
toKeyPath:#"listID"
withMapping:listEntityMapping]];
a runtime error occurs saying "Unable to add mapping for keyPath listID, one already exists"
and if i use this
[listEntityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"listID"
toKeyPath:#"list.listID"
withMapping:taskEntityMapping]];
app crashes with this error the entity List is not key value coding-compliant for the key "list".'
and if i omit the above code and try to get listID from relationship
task.list.listID
it gives me "0". Can anyone tell me what exactly am i doing wrong or what should i do to accomplish above task. I can give more details on this if needed.
EDIT
but my request to all list returns following json
GET www.mydomain.com/api/lists
[
{"listID":"42","listName":"List 4","listSyncStatus":"1"},
{"listID":"41","listName":"List 3","listSyncStatus":"1"},
{"listID":"40","listName":"List 2","listSyncStatus":"1"}
]
and request to single list will return its task as follows
GET www.mydomain.com/api/list/42
[
{"taskID":"22","listID":"42","taskName":"Task 2","taskSyncStatus":"1","taskCompletionStatus":"1"},
{"taskID":"21","listID":"42","taskName":"Task 1","taskSyncStatus":"1","taskCompletionStatus":"1"}
]
i.e there is no cascading relationship in returned in json. is this wrong way or what am i missing here ?
Corrected After Accepting Answer
It turns out i was returning wrong json i.e. the returned json has no relationship in it while the iOS model has a relationship "tasks" so i edited my rest api to return correct nested json which is like below
[ { "listID" : "96",
"listName" : "List 1",
"listSyncStatus" : "1",
"tasks" : [ { "taskCompletionStatus" : "1",
"taskID" : "67",
"taskName" : "Task 2",
"taskSyncStatus" : "1"
},
{ "taskCompletionStatus" : "1",
"taskID" : "66",
"taskName" : "Task 1",
"taskSyncStatus" : "1"
}
]
},
{ "listID" : "97",
"listName" : "List 2",
"listSyncStatus" : "1",
"tasks" : [ { "taskCompletionStatus" : "1",
"taskID" : "69",
"taskName" : "Task 1",
"taskSyncStatus" : "1"
},
{ "taskCompletionStatus" : "1",
"taskID" : "68",
"taskName" : "Task 1",
"taskSyncStatus" : "1"
}
]
}
]
after returning above nested json, everything works like charm, specially this line
[listEntityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"tasks"
toKeyPath:#"tasks"
withMapping:taskEntityMapping]];
Hope this helps grasping relationship concepts for people like me out there.
You modal diagram shows that you have one-to-many relationship (One list has many tasks).
As far as i know, in this case you need to add relationship mapping on List entity only, no need on Task entity. Also for one-to-many relationship, you don't need to add list relationship under Task entity.
Your entities relationship should look like this
So try following
[listEntityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"YOUR_JSON_KEYPATH_HERE" toKeyPath:#"tasks" withMapping: taskEntityMapping]];
IMPORTANT
In above method
FromKeyPath parameter should be the name of your JSON key where the relationship starts.
toKeyPath parameter should be the relationship name that you have mentioned in Entity diagram. i.e; tasks.
withMapping should be the mapping of many entity. In you case taskEntityMapping
Hope this fix the issue.
Not sure why this is not working, but you can try to create the relationship mapping like this:
[taskEntityMapping addConnectionForRelationship:#"list" connectedBy:#{ #"listId": #"listId" }];
[listEntityMapping addConnectionForRelationship:#"tasks" connectedBy:#{ #"listId":#"listId"}];

Using Keys as Values in RestKit

So, we have an API that spits out:
{
"2013-12-13": [subobject1, subobject2, subobjects3],
"2013-12-14": [subobject4, subobject5],
...
}
I'm not sure, though, how to parse things in RestKit where the key changes dynamically.
I'd probably like to parse it into a list of objects that have a date, and the list of subobjects.
Any thoughts?
You need to create a mapping something like:
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[XXX class]];
[mapping addAttributeMappingFromKeyOfRepresentationToAttribute:#"date"];
[mapping addAttributeMappingsFromDictionary:#{
#"(date)": #"values"
}];
Where your XXX class has properties for date (NSDate) and values (NSArray).

Multiple RKRelationshipMapping with same toKeyPath

I have a problem using multiple RKRelationshipMappings with same toKeyPath (destinationKeyPath).
I have had success with using one RKRelationshipMapping to one toKeyPath but I'm unable to attach a second RKRelationshipMappings with the same toKeyPath.
Code with one RKRelationshipMapping that works:
RKObjectMapping *someObjectRequestMapping = [RKObjectMapping requestMapping];
[someObjectRequestMapping addAttributeMappingsFromDictionary:#{#"prop1" : #"prop_1", #"prop2" : #"prop_2"}];
RKObjectMapping *firstSubObjectMapping = [RKObjectMapping requestMapping];
[firstSubObjectMapping addAttributeMappingsFromDictionary:#{#"subProp1" : #"sub_prop1", #"subProp2" : #"sub_prop2"}];
[someObjectRequestMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"prop3"
toKeyPath:#"prop_3"
withMapping:firstSubObjectMapping];
However, if i want to add another RKRelationshipMapping to "someObjectRequestMapping" with the same toKeyPath ("prop_3") the RestKit fails with error:
*'NSInternalInconsistencyException', reason: 'Unable to add mapping for keyPath invites_attributes, one already exists...'*
Code with two RKRelationshipMappings that fails:
RKObjectMapping *someObjectRequestMapping = [RKObjectMapping requestMapping];
[someObjectRequestMapping addAttributeMappingsFromDictionary:#{#"prop1" : #"prop_1", #"prop2" : #"prop_2"}];
RKObjectMapping *firstSubObjectMapping = [RKObjectMapping requestMapping];
[firstSubObjectMapping addAttributeMappingsFromDictionary:#{#"subProp1" : #"sub_prop1", #"subProp2" : #"sub_prop2"}];
RKObjectMapping *secondSubObjectMapping = [RKObjectMapping requestMapping];
[secondSubObjectMapping addAttributeMappingsFromDictionary:#{#"subProp2" : #"sub_prop2", #"subProp3" : #"sub_prop3"}];
[someObjectRequestMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"prop3"
toKeyPath:#"prop_3"
withMapping:firstSubObjectMapping];
[someObjectRequestMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"prop4"
toKeyPath:#"prop_3"
withMapping:secondSubObjectMapping];
The mapping is used in RKRequestDescriptor.
Any help would be greatly appreciated.
Best Regards,
Damir
The error is quite clear, RestKit will not allow ambiguity so you can only have one mapping per key path.
Usually if this was for a response descriptor you would use a dynamic mapping to determine which relationship mapping was appropriate. For a request descriptor you can do something similar yourself where you analyse the object to be sent and decide which mapping is appropriate. This does mean having 2 different mappings and choosing between them manually. This could also be done using multiple RKObjectManagers, each configured with a different set of mappings.

Mapping relationships in RestKit through an array of IDs doesn't work

I'm trying to map users and groups to CoreData objects through RestKit, maintaining the relationship between the two.
The JSON for the users is something like
{"users":
[{"fullname": "John Doe", "_id": "1"}
...
]
}
and the JSON for the groups is something like
{"groups":
[{"name": "John's group", "_id": "a",
"members": ["1", "2"]
...
]
}
I also have corresponding Core Data objects, User and Group, with a one-to-many relationship mapping between groups and users. The relationship property in Group is called members and a separate NSArray called memberIDs holds the array of string IDs of all member users.
What I want to accomplish in RestKit is to load these objects and have the relationship mapped for me. The code for loading the users is straight forward standard stuff, and the code for loading the groups is something like
RKObjectManager* objectManager = [RKObjectManager sharedManager];
RKManagedObjectMapping* groupMapping = [RKManagedObjectMapping mappingForClass:[Group class] inManagedObjectStore:objectManager.objectStore];
groupMapping.primaryKeyAttribute = #"identifier";
[groupMapping mapKeyPath:#"_id" toAttribute:#"identifier"];
[groupMapping mapKeyPath:#"name" toAttribute:#"name"];
[groupMapping mapKeyPath:#"members" toAttribute:#"memberIDs"];
[objectManager.mappingProvider setMapping:groupMapping forKeyPath:#"groups"];
// Create an empty user mapping to handle the relationship mapping
RKManagedObjectMapping* userMapping = [RKManagedObjectMapping mappingForClass:[User class] inManagedObjectStore:objectManager.objectStore];
[groupMapping hasMany:#"members" withMapping:userMapping];
[groupMapping connectRelationship:#"members" withObjectForPrimaryKeyAttribute:#"memberIDs"];
RKObjectRouter* router = objectManager.router;
[router routeClass:[Group class] toResourcePath:#"/rest/groups/:identifier"];
[router routeClass:[Group class] toResourcePath:#"/rest/groups/" forMethod:RKRequestMethodPOST];
// Assume url is properly defined to point to the right path...
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:url usingBlock:^(RKObjectLoader *loader) {}];
When I run this code, I get the following warning spit out several times (seems to be about twice per relationship):
2012-10-24 08:55:10.170 Test[35023:18503] W restkit.core_data.cache:RKEntityByAttributeCache.m:205 Unable to add object with nil value for attribute 'identifier': <User: 0x86f7fc0> (entity: User; id: 0x8652400 <x-coredata:///User/t01A9EDBD-A523-4806-AFC2-9B06873D764E531> ; data: {
fullname = nil;
identifier = nil;
})
The strange thing is that the relationship gets set up properly (even looked at the sqlite database, and everything looks fine there) but I'm not happy with something clearly going wrong in the RestKit code, and it seems to have something to do with the bogus User mapping I create in the code above (it's empty, since there is nothing to map in the array of IDs).
I've tried alternatives, for example when I add a key path mapping:
[userMapping mapKeyPath:#"" toAttribute:#"identifier"];
It complains, obviously because the key path is empty
2012-10-24 09:24:11.735 Test[35399:14003] !! Uncaught exception !!
[<__NSCFString 0xa57f460> valueForUndefinedKey:]: this class is not key value coding-compliant for the key .
And if I try to use the real User mapping
[groupMapping hasMany:#"members" withMapping:[[RKObjectManager sharedManager].mappingProvider objectMappingForKeyPath:#"users"]];
I get the following error
2012-10-24 09:52:49.973 Test[35799:18403] !! Uncaught exception !!
[<__NSCFString 0xa56e9a0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key _id.
2012-10-24 09:52:49.973 Test[35799:18403] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0xa56e9a0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key _id.'
Seems like RestKit is trying to map the ID strings themselves in the memberIDs array to User objects, which doesn't make any sense. I've seen examples where the relationship array is a list of dictionaries with keys (e.g. called id) with the reference ID to the other object, for example:
{"groups":
[{"name": "John's group", "_id": "a",
"members": [
{"_id": "1"},
{"_id": "2"}
]
...
]
}
But I don't want to change my JSON in that way (besides, I'm hoping RestKit is flexible enough to support the other way.)
Does anyone know what the proper way of doing this kind of relationship mapping in RestKit is?
Update
I ended up modifying the REST interface to send a list of dictionaries containing the user object IDs (just like the last example above), and got that to work. Still not completely happy with the setup, but the code now looks like
RKObjectManager* objectManager = [RKObjectManager sharedManager];
RKManagedObjectMapping* groupMapping = [RKManagedObjectMapping mappingForClass:[Group class] inManagedObjectStore:objectManager.objectStore];
groupMapping.primaryKeyAttribute = #"identifier";
[groupMapping mapKeyPath:#"_id" toAttribute:#"identifier"];
[groupMapping mapKeyPath:#"name" toAttribute:#"name"];
[groupMapping mapKeyPath:#"members._id" toAttribute:#"memberIDs"];
[objectManager.mappingProvider setMapping:groupMapping forKeyPath:#"groups"];
// Create an empty user mapping to handle the relationship mapping
RKManagedObjectMapping* userMapping = [RKManagedObjectMapping mappingForClass:[User class] inManagedObjectStore:objectManager.objectStore];
userMapping.primaryKeyAttribute = #"identifier";
[userMapping mapKeyPath:#"_id" toAttribute:#"identifier"];
[groupMapping hasMany:#"members" withMapping:userMapping];
[groupMapping connectRelationship:#"members" withObjectForPrimaryKeyAttribute:#"memberIDs"];
RKObjectRouter* router = objectManager.router;
[router routeClass:[Group class] toResourcePath:#"/rest/groups/:identifier"];
[router routeClass:[Group class] toResourcePath:#"/rest/groups/" forMethod:RKRequestMethodPOST];
// Assume url is properly defined to point to the right path...
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:url usingBlock:^(RKObjectLoader *loader) {}];
Just in case this helps anyone else in a similar situation. There might still be some problems with the approach I've taken, but so far so good (it even handles out of order loading, which is nice) - would still love to hear from anyone if there's an answer to my original question.
Found a way to make this work, and also realized that the updated approach in my question is only supposed to be used for nested objects, not referenced objects. This thread got me on the right track: https://groups.google.com/forum/#!msg/restkit/swB1Akv2lTE/mnP2OMSqElwJ (worth reading if you're dealing with relationships in RestKit)
Assume the groups JSON still looks like the original JSON:
{"groups":
[{"name": "John's group", "_id": "a",
"members": ["1", "2"]
...
]
}
Also assume that users are mapped to a keypath users (i.e. as in something like [objectManager.mappingProvider setMapping:userMapping forKeyPath:#"users"];) the correct way to map the relationships looks like this:
RKObjectManager* objectManager = [RKObjectManager sharedManager];
RKManagedObjectMapping* groupMapping = [RKManagedObjectMapping mappingForClass:[Group class] inManagedObjectStore:objectManager.objectStore];
groupMapping.primaryKeyAttribute = #"identifier";
[groupMapping mapKeyPath:#"_id" toAttribute:#"identifier"];
[groupMapping mapKeyPath:#"name" toAttribute:#"name"];
[groupMapping mapKeyPath:#"members" toAttribute:#"memberIDs"];
[objectManager.mappingProvider setMapping:groupMapping forKeyPath:#"groups"];
[groupMapping mapKeyPath:#"users" toRelationship:#"members" withMapping:[[RKObjectManager sharedManager].mappingProvider objectMappingForKeyPath:#"users"] serialize:NO];
[groupMapping connectRelationship:#"members" withObjectForPrimaryKeyAttribute:#"memberIDs"];
RKObjectRouter* router = objectManager.router;
[router routeClass:[Group class] toResourcePath:#"/rest/groups/:identifier"];
[router routeClass:[Group class] toResourcePath:#"/rest/groups/" forMethod:RKRequestMethodPOST];
// Assume url is properly defined to point to the right path...
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:url usingBlock:^(RKObjectLoader *loader) {}];
When I had tried this approach earlier, the thing that confused me was this line
[groupMapping mapKeyPath:#"users" toRelationship:#"members" withMapping:[[RKObjectManager sharedManager].mappingProvider objectMappingForKeyPath:#"users"] serialize:NO];
where the first key path is referring to the key path mapping for the objects being referenced (users in this case), not the key path of the relationship within the group object being mapped (which would have been members in this case).
With this change, all relationships work as expected and I'm happy.

How to map a JSON array with RestKit

I have json string like this format :
[{"image":"/0001.jpg","link":"/index.php"},
{"image":"/0001.jpg","link":"/index.php"}]
it does not have a key in the top level.
[mapping mapKeyPath:#"image" toAttribute:#"image"];
mapping like this won't work , it give me the error:
restkit.object_mapping:RKObjectMapper.m:81 Adding mapping error: Could not find an object mapping for keyPath: ''
How to map this type of json ?
Use
[[RKObjectManager sharedManager].mappingProvider addObjectMapping:myObject];
You should check "Mapping without KVC" section on Restkit Object Mapping Docs.
Here is an example from the docs:
[
{ "title": "RestKit Object Mapping Intro",
"body": "This article details how to use RestKit object mapping...",
"author": {
"name": "Blake Watters",
"email": "blake#restkit.org"
},
"publication_date": "7/4/2011"
}
]
And you map that like this:
// Our familiar articlesMapping from earlier
RKObjectMapping* articleMapping = [RKObjectMapping mappingForClass:[Article class]];
[articleMapping mapKeyPath:#"title" toAttribute:#"title"];
[articleMapping mapKeyPath:#"body" toAttribute:#"body"];
[articleMapping mapKeyPath:#"author" toAttribute:#"author"];
[articleMapping mapKeyPath:#"publication_date" toAttribute:#"publicationDate"];
[[RKObjectManager sharedManager].mappingProvider addObjectMapping:articleMapping];
For me the solution was:
add this response descriptor, use the object inside de array
[manager addResponseDescriptor:[ObjectInsideTheArray getResponseDescriptor]];
and in the success response
NSArray *response = (NSArray *)[mappingResult array]; //instead of firstObject

Resources