Please skip this part to UPDATE 3.
Environment: iOS7.1, MagicalRecord, Restkit, Mogenerator
The required JSON form looks like this:
{"amount":[10,20,50]} (set of TSNDecimal's in TSNTagAmountRequest)
The following entities are involved in the request:
@interface TSNTagAmountRequest: NSManagedObject
@property (nonatomic, strong) NSSet *amount;
@end
where the set of amounts is set of TSNDecimal's:
@interface TSNDecimal: NSManagedObject
@property (nonatomic, strong) NSDecimalNumber* number;
@end
The decimal number with name number is a helper.
RKObjectMapping for TSNDecimal:
RKEntityMapping *decimalMapping = [RKEntityMapping
mappingForEntityForName:@"TSNDecimal"
inManagedObjectStore:managedObjectStore];
// map the value in JSON directly to the helper "number"
[decimalMapping addPropertyMapping:
[RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@"number"]];
The previous line is the code I would expect to deserialize correctly "amount":[10,20,50] to three TSNDecimal's, respectively to their number attributes (it works) and also it should serialize properly from set of TSNDecimal (via number) to JSON: "amount":[10,20..] It works when parsing response but not for the request.
The rest is the relationship mappings:
RKPropertyMapping* decimalPropertyMappingAmount =
[RKRelationshipMapping relationshipMappingFromKeyPath:@"amount"
toKeyPath:@"amount"
withMapping:decimalMapping];
(I also tried FromKeyPath:@"amount.number" toKeyPath:@"amount" but an exception is being raised because of passing an incorrect path to KVC ([self.object valueForKey:key]))
Finally I have defined the relationship between TSNTagAmountRequest and TSNDecimal: tagAmountRequestMapping = [RKObjectMapping requestMapping]; [tagAmountRequestMapping addPropertyMapping:decimalPropertyMappingAmount];
and add the descriptor to the RK manager:
requestDescriptor = [RKRequestDescriptor
requestDescriptorWithMapping:tagAmountRequestMapping
objectClass:[TSNTagAmountRequest class]
rootKeyPath:nil
method:RKRequestMethodPOST];
[_rkManager addRequestDescriptor:requestDescriptor];
The issue is that there is an exception raised in NSJSONSerialization dataWithJSONObject:optionserror: obviously because the TSNDecimal is not translated from TSNDecimal.number to NSDecimalNumber which NSJSONSerialization understands.
Exception: Invalid type in JSON write (TSNDecimal) because the data coming in dataFromObject in RKNSJSONSerialization contains TSNDecimal objects created by RKObjectMappingOperationDataSource (not RKManagedObjectMappingOperationDataSource)
{
amount = (
"<TSNDecimal: 0xc1d0bf0> (entity: (null); id: (null) ; data: {\n})",
"<TSNDecimal: 0x11543080> (entity: (null); id: (null) ; data: {\n})"
);
}
it should be NSDecimalNumber instead of TSNDecimal I guess.
In summary I was expecting the code
[decimalMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@"number"]];
does its job and translates "number" back to nil (that is, to unnamed item in the collection) but it does not work this way. There is this example in the section Mapping Values without Key Paths but I am confused by using [RKResponseDescriptor responseDescriptorWithMapping: if I want to compose the JSON body. Parsing from the response is working well with
[decimalMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@"number"]];
RKPropertyMapping* decimalPropertyMappingAmount =
[RKRelationshipMapping relationshipMappingFromKeyPath:@"amount"
toKeyPath:@"amount"
withMapping:decimalMapping];
why not for request?
The issue I see there is CoreData: error: Failed to call designated initializer on NSManagedObject class 'TSNDecimal'
The core data objects are initialized with
TSNTagAmountRequest* request = [TSNTagAmountRequest requestWithModel:self];
TSNDecimal* decimalEntity = nil;
for(NSDecimalNumber* number in amount) {
decimalEntity = [TSNDecimal MR_createEntity];
decimalEntity.number = number;
[request addAmountObject:decimalEntity];
}
Here is the log:
D restkit.object_mapping:RKMappingOperation.m:860 Starting mapping operation...
T restkit.object_mapping:RKMappingOperation.m:861 Performing mapping operation: <RKMappingOperation 0x9681ca0> for '__NSDictionaryM' object. Mapping values from object <TSNTagAmountRequest: 0xf7b8c90> (entity: TSNTagAmountRequest; id: 0xf784700 <x-coredata:///TSNTagAmountRequest/tB2DF8887-7E6C-420F-998A-D250751E5CED164> ; data: {
amount = (
"0x9664220 <x-coredata:///TSNDecimal/tB2DF8887-7E6C-420F-998A-D250751E5CED165>",
"0x9616390 <x-coredata:///TSNDecimal/tB2DF8887-7E6C-420F-998A-D250751E5CED166>"
);
}) ((null)) to object {
} with object mapping (null)
D restkit.object_mapping:RKMappingOperation.m:641 Mapping one to many relationship value at keyPath 'amount' to 'amount'
CoreData: error: Failed to call designated initializer on NSManagedObject class 'TSNDecimal'
T restkit.object_mapping:RKMappingOperation.m:542 Performing nested object mapping using mapping <RKRelationshipMapping: 0x95a0df0 amount => amount> for data: <TSNDecimal: 0x9652900> (entity: TSNDecimal; id: 0x9664220 <x-coredata:///TSNDecimal/tB2DF8887-7E6C-420F-998A-D250751E5CED165> ; data: {
number = 5;
})
D restkit.object_mapping:RKMappingOperation.m:860 Starting mapping operation...
T restkit.object_mapping:RKMappingOperation.m:861 Performing mapping operation: <RKMappingOperation 0x96b3d60> for 'TSNDecimal' object. Mapping values from object <TSNDecimal: 0x9652900> (entity: TSNDecimal; id: 0x9664220 <x-coredata:///TSNDecimal/tB2DF8887-7E6C-420F-998A-D250751E5CED165> ; data: {
number = 5;
}) ({
mapping = {
collectionIndex = 3221208359;
};
}) to object <TSNDecimal: 0x96b2880> (entity: (null); id: (null) ; data: {
}) with object mapping (null)
T restkit.object_mapping:RKMappingOperation.m:439 Found transformable value at keyPath '(null)'. Transforming from class 'TSNDecimal' to 'NSDecimalNumber'
E restkit.object_mapping:RKMappingOperation.m:441 Failed transformation of value at keyPath '(null)' to representation of type 'NSDecimalNumber': Error Domain=org.restkit.RKValueTransformers.ErrorDomain Code=3002 "Failed transformation of value '<TSNDecimal: 0x9652900> (entity: TSNDecimal; id: 0x9664220 <x-coredata:///TSNDecimal/tB2DF8887-7E6C-420F-998A-D250751E5CED165> ; data: {
number = 5;
}) ({
mapping = {
collectionIndex = 3221208359;
};
})' to NSDecimalNumber: none of the 2 value transformers consulted were successful." UserInfo=0x96bd900 {detailedErrors=(
"Error Domain=org.restkit.RKValueTransformers.ErrorDomain Code=3002 \"The given value is not already an instance of 'NSDecimalNumber'\" UserInfo=0x96bd630 {NSLocalizedDescription=The given value is not already an instance of 'NSDecimalNumber'}",
"Error Domain=org.restkit.RKValueTransformers.ErrorDomain Code=3000 \"Expected an `inputValue` of type `NSNull`, but got a `TSNDecimal`.\" UserInfo=0x96bd720 {NSLocalizedDescription=Expected an `inputValue` of type `NSNull`, but got a `TSNDecimal`.}"
), NSLocalizedDescription=Failed transformation of value '<TSNDecimal: 0x9652900> (entity: TSNDecimal; id: 0x9664220 <x-coredata:///TSNDecimal/tB2DF8887-7E6C-420F-998A-D250751E5CED165> ; data: {
number = 5;
}) ({
mapping = {
collectionIndex = 3221208359;
};
})' to NSDecimalNumber: none of the 2 value transformers consulted were successful.}
T restkit.object_mapping:RKMappingOperation.m:519 Did not find mappable attribute value keyPath '(null)'
D restkit.object_mapping:RKMappingOperation.m:905 Mapping operation did not find any mappable values for the attribute and relationship mappings in the given object representation
D restkit.object_mapping:RKMappingOperation.m:927 Failed mapping operation: No mappable values found for any of the attributes or relationship mappings
W restkit.object_mapping:RKMappingOperation.m:553 WARNING: Failed mapping nested object: No mappable values found for any of the attributes or relationship mappings
The debugger show these mappings in -(BOOL)mapOneToManyRelationshipWithValue:mapping:
(lldb) po relationshipMapping
<RKRelationshipMapping: 0x93d8330 amount => amount>
(lldb) po relationshipMapping.mapping
<RKEntityMapping:0x93b7d50 objectClass=TSNDecimal propertyMappings=
(
"<RKAttributeMapping: 0x93b8e60 (null) => number>"
)>
UPDATE 1
I noticed that when TSNDecimal is being serialized (or right before the mapping process) it is created again in RKMappingOperation.m, in -(id)destinationObjectForMappingRepresentation:parentRepresentation:withMapping:inRelationship: on line
return [self.dataSource mappingOperation:self targetObjectForRepre ...
with the wrong constructor new (that is why I see the coredata error: CoreData: error: Failed to call designated initializer on NSManagedObject class) later in the log. The self.dataSource calls wrong mappingOperation:targetObjectForRepresentation: as it is dataSource for non-managed objects (RKObjectMappingOperationDataSource). It should be RKManagedObjectMappingOperationDataSource.
I guessed it might be related issue to the RKObjectMapping v. RKEntityMapping problem. But when I use RKEntityMapping for TSNTagAmountRequest:
tagAmountRequestMapping = [RKEntityMapping mappingForEntityForName:[TSNTagAmountRequest class] inManagedObjectStore:managedObjectStore];
then I get the error: ** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'RKRequestDescriptor
objects must be initialized with a mapping whose target class is NSMutableDictionary
, got 'TSNTagAmountRequest' (see [RKObjectMapping requestMapping]
)'**
UPDATE 2
I have changed the implementation so that it uses inverse mappings:
RKObjectMapping* decimalMappingInverse = [(RKEntityMapping*)decimalMapping inverseMapping];
RKPropertyMapping* decimalPropertyMappingAmountInverse =
[RKRelationshipMapping relationshipMappingFromKeyPath:@"amount"
toKeyPath:@"amount"
withMapping:decimalMappingInverse];
[tagAmountRequestMapping addPropertyMapping:decimalPropertyMappingAmountInverse];
requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:tagAmountRequestMapping
objectClass:[TSNTagAmountRequest class]
rootKeyPath:nil
method:RKRequestMethodPOST];
[_rkManager addRequestDescriptor:requestDescriptor];
With output with exception at the end of mapping operation Invalid (non-string) key in JSON dictionary:
(
{
5 = {
};
},
{
100 = {
};
}
)
And the log:
D restkit.object_mapping:RKMappingOperation.m:860 Starting mapping operation...
T restkit.object_mapping:RKMappingOperation.m:861 Performing mapping operation: <RKMappingOperation 0xf844d50> for '__NSDictionaryM' object. Mapping values from object <TSNTicketAmountApiRequest: 0xf80e9b0> (entity: TSNTicketAmountApiRequest; id: 0xf86bbe0 <x-coredata:///TSNTicketAmountApiRequest/t5BFB6379-F8EB-4367-A53B-E1B3C531A34F66> ; data: {
amount = (
"0xf8c3b30 <x-coredata:///TSNDecimalApiTO/t5BFB6379-F8EB-4367-A53B-E1B3C531A34F67>",
"0xf80c370 <x-coredata:///TSNDecimalApiTO/t5BFB6379-F8EB-4367-A53B-E1B3C531A34F68>"
);
}) ((null)) to object {
} with object mapping (null)
T restkit.object_mapping:RKMappingOperation.m:454 Mapping attribute value keyPath 'token' to 'token'
T restkit.object_mapping:RKMappingOperation.m:470 Mapped attribute value from keyPath 'token' to 'token'. Value: SXppH1ajA9OspKHkQ6Uwi7dD7AoYVP7J:api
T restkit.object_mapping:RKMappingOperation.m:454 Mapping attribute value keyPath 'bonification' to 'bonification'
T restkit.object_mapping:RKMappingOperation.m:470 Mapped attribute value from keyPath 'bonification' to 'bonification'. Value: 1
D restkit.network:RKObjectParameterization.m:129 Serialized NSDecimalNumber value at keyPath to __NSCFString (1)
T restkit.object_mapping:RKMappingOperation.m:454 Mapping attribute value keyPath 'isNetCurrency' to 'isNetCurrency'
T restkit.object_mapping:RKMappingOperation.m:470 Mapped attribute value from keyPath 'isNetCurrency' to 'isNetCurrency'. Value: 0
D restkit.network:RKObjectParameterization.m:129 Serialized __NSCFNumber value at keyPath to __NSCFBoolean (0)
D restkit.object_mapping:RKMappingOperation.m:641 Mapping one to many relationship value at keyPath 'amount' to 'amount'
T restkit.object_mapping:RKMappingOperation.m:542 Performing nested object mapping using mapping <RKRelationshipMapping: 0xf87bf40 amount => amount> for data: <TSNDecimalApiTO: 0xf8f0a60> (entity: TSNDecimalApiTO; id: 0xf8c3b30 <x-coredata:///TSNDecimalApiTO/t5BFB6379-F8EB-4367-A53B-E1B3C531A34F67> ; data: {
number = 5;
})
D restkit.object_mapping:RKMappingOperation.m:860 Starting mapping operation...
T restkit.object_mapping:RKMappingOperation.m:861 Performing mapping operation: <RKMappingOperation 0x93779f0> for '__NSDictionaryM' object. Mapping values from object <TSNDecimalApiTO: 0xf8f0a60> (entity: TSNDecimalApiTO; id: 0xf8c3b30 <x-coredata:///TSNDecimalApiTO/t5BFB6379-F8EB-4367-A53B-E1B3C531A34F67> ; data: {
number = 5;
}) ({
mapping = {
collectionIndex = 3221208359;
};
}) to object {
} with object mapping (null)
T restkit.object_mapping:RKMappingOperation.m:439 Found transformable value at keyPath 'number'. Transforming from class 'NSDecimalNumber' to 'NSMutableDictionary'
T restkit.object_mapping:RKMappingOperation.m:454 Mapping attribute value keyPath 'number' to '(null)'
T restkit.object_mapping:RKMappingOperation.m:470 Mapped attribute value from keyPath 'number' to '(null)'. Value: {
5 = {
};
}
D restkit.object_mapping:RKMappingOperation.m:929 Finished mapping operation successfully...
T restkit.object_mapping:RKMappingOperation.m:542 Performing nested object mapping using mapping <RKRelationshipMapping: 0xf87bf40 amount => amount> for data: <TSNDecimalApiTO: 0xf8f0ea0> (entity: TSNDecimalApiTO; id: 0xf80c370 <x-coredata:///TSNDecimalApiTO/t5BFB6379-F8EB-4367-A53B-E1B3C531A34F68> ; data: {
number = 100;
})
D restkit.object_mapping:RKMappingOperation.m:860 Starting mapping operation...
T restkit.object_mapping:RKMappingOperation.m:861 Performing mapping operation: <RKMappingOperation 0x9311160> for '__NSDictionaryM' object. Mapping values from object <TSNDecimalApiTO: 0xf8f0ea0> (entity: TSNDecimalApiTO; id: 0xf80c370 <x-coredata:///TSNDecimalApiTO/t5BFB6379-F8EB-4367-A53B-E1B3C531A34F68> ; data: {
number = 100;
}) ({
mapping = {
collectionIndex = 3221208359;
};
}) to object {
} with object mapping (null)
T restkit.object_mapping:RKMappingOperation.m:439 Found transformable value at keyPath 'number'. Transforming from class 'NSDecimalNumber' to 'NSMutableDictionary'
T restkit.object_mapping:RKMappingOperation.m:454 Mapping attribute value keyPath 'number' to '(null)'
T restkit.object_mapping:RKMappingOperation.m:470 Mapped attribute value from keyPath 'number' to '(null)'. Value: {
};
}
D restkit.object_mapping:RKMappingOperation.m:929 Finished mapping operation successfully...
T restkit.object_mapping:RKMappingOperation.m:682 Mapped relationship object from keyPath 'amount' to 'amount'. Value: (
{
5 = {
};
},
{
100 = {
};
}
)
In addition to the above exception the result does not match the required body {"amount":[5,100]}:
D restkit.object_mapping:RKMappingOperation.m:929 Finished mapping operation successfully...
exception Invalid (non-string) key in JSON dictionary
UPDATE 3
This issue boils down to the way how the array of decimal numbers from JSON are parsed to the NSSet of NSManagedObjects (working from very first moment) and vice versa, from NSSet to JSON array (quite frustrating to get it work):
{"amount":[10,20,50]}
that is
TSNTagAmountRequest:{amount:[TSNDecimal, TSNDecimal, TSNDecimal, ...]}
Where
@interface TSNTagAmountRequest : NSManagedObject
@property(nonatomic) NSSet *amount;
@end
and
@interface TSNDecimal : NSManagedObject
@property(nonatomic) NSDecimalNumber* number; // is a helper property
@end
RKEntityMapping* tagAmountRequestMapping = [RKObjectMapping requestMapping];
[tagAmountRequestMapping
addPropertyMapping:decimalPropertyMappingAmountInverse];
where
RKPropertyMapping* decimalPropertyMappingAmountInverse =
[RKRelationshipMapping
relationshipMappingFromKeyPath:@"amount"
toKeyPath:@"amount"
withMapping:decimalMappingInverse];
where
RKObjectMapping* decimalMappingInverse = [(RKEntityMapping*)decimalMapping inverseMapping];
where
RKObjectMapping* decimalMapping = [RKEntityMapping mappingForEntityForName:@"TSNDecimal" inManagedObjectStore:managedObjectStore];
[mapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:nil
toKeyPath:@"number"]];
requestDescriptor = [RKRequestDescriptor
requestDescriptorWithMapping:tagAmountRequestMapping
objectClass:[TSNTagAmountRequest class]
rootKeyPath:TSNServiceClientRemoteRootKeyPathNotNested
method:RKRequestMethodPOST];
[_rkManager addRequestDescriptor:requestDescriptor];
The method + (NSData *)dataFromObject:error: from RKNSJSONSerialization cannot consume the result object from mapping containing NSDictionary :
(
{
5 = {
};
},
{
100 = {
};
}
)
where I expect something like [5, 100].
The app crashes with Invalid (non-string) key in JSON dictionary.
I do not understand why [NSJSONSerialization dataWithJSONObject:object options:0 error:error]; is served this strange array of dictionaries instead of the array of NSDecimalNumbers.
UPDATE 4
I have changed the definition of the relationship mapping between TSNTagAmountRequest and TSNDecimal so that the value in TSNDecimal (item from amount NSSet) is propagated with its attribute "number".
RKPropertyMapping* decimalPropertyMappingAmountInverse =
[RKRelationshipMapping relationshipMappingFromKeyPath:@"amount.number"
toKeyPath:@"amount"
withMapping:decimalApiMappingInverse];
Restkit engine, exactly applyRelationshipMappings method from RKMappingOperation.m is able to extract amount values 5, 100 but the final JSON is assembled incorrectly anyway:
"amount":[{},{}]
with missing values. I'm almost there.
UPDATE 5
After setting up the inverseMapping's for all involved entities, feeding the request descriptor, still the output (driven by the mapping number->nil) is assembled incorrectly with dictionary of dictionaries, not a JSON array. The only way is to subclass RKNSJSONSerialization and fix the structure, that is, to translate the dictionary of dictionaries to simple nsarray which is compatible with ns json serializer.