Question

I am having some problems using an RKValueTransformer to serialize out an NSData image bytes to a base64 encoded string for a request. I was able to do the inverse for a response, after some help I received on stackoverflow.

Here is my code for creating the NSString to NSData value transformer, which works without issue. I found the index of the null value transformer and set it at afterNullTransformerIndex. I have also set it at index 0, but then I have to do my own null checking and this seems to work without issue.

    //add the base64 to NSData transformer after the null value transformer
RKBlockValueTransformer *base64StringToNSDataTransformer = [RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class inputValueClass, __unsafe_unretained Class outputValueClass) {
    return [inputValueClass isSubclassOfClass:[NSString class]] && [outputValueClass isSubclassOfClass:[NSData class]];
} transformationBlock:^BOOL(id inputValue, __autoreleasing id *outputValue, __unsafe_unretained Class outputClass, NSError *__autoreleasing *error) {
    RKValueTransformerTestInputValueIsKindOfClass(inputValue, [NSString class], error);
    RKValueTransformerTestOutputValueClassIsSubclassOfClass(outputClass, [NSData class], error);

    *outputValue = [[NSData alloc] initWithBase64EncodedString:(NSString *)inputValue options:NSDataBase64DecodingIgnoreUnknownCharacters];

    return YES;
}];
base64StringToNSDataTransformer.name = @"base64StringToNSDataTransformer";
[[RKValueTransformer defaultValueTransformer] insertValueTransformer:base64StringToNSDataTransformer atIndex:afterNullTransformerIndex];

And this is my code for creating the NSData to NSString value transformer, which isn't working. I set a breakpoint in the transformationBlock: method, but it never gets invoked.:

    //add the NSData to String transformer for requests after the null value transformer
RKBlockValueTransformer *nsDataToBase64StringTransformer = [RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class inputValueClass, __unsafe_unretained Class outputValueClass) {
    return [inputValueClass isSubclassOfClass:[NSData class]] && [outputValueClass isSubclassOfClass:[NSString class]];
} transformationBlock:^BOOL(id inputValue, __autoreleasing id *outputValue, __unsafe_unretained Class outputClass, NSError *__autoreleasing *error) {
    RKValueTransformerTestInputValueIsKindOfClass(inputValue, [NSData class], error);
    RKValueTransformerTestOutputValueClassIsSubclassOfClass(outputClass, [NSString class], error);

    *outputValue = [((NSData *)inputValue) base64EncodedStringWithOptions:NSDataBase64Encoding76CharacterLineLength];

    return YES;
}];
nsDataToBase64StringTransformer.name = @"nsDataToBase64StringTransformer";
[[RKValueTransformer defaultValueTransformer] insertValueTransformer:nsDataToBase64StringTransformer atIndex:afterNullTransformerIndex];

Like I said, my breakpoint never gets invoked in the transformationBlock: method, but the valueTransformationWithValidationBlock: does get invoked once when serializing the request, but only when transforming from a Date to a String. Looking through the stack in the debugger and RestKit's code, I found this method in RKObjectParameterization.m:

- (void)mappingOperation:(RKMappingOperation *)operation didSetValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKAttributeMapping *)mapping
{
    id transformedValue = nil;
    if ([value isKindOfClass:[NSDate class]]) {
        [mapping.objectMapping.valueTransformer transformValue:value toValue:&transformedValue ofClass:[NSString class] error:nil];
    } else if ([value isKindOfClass:[NSDecimalNumber class]]) {
        // Precision numbers are serialized as strings to work around Javascript notation limits
        transformedValue = [(NSDecimalNumber *)value stringValue];
    } else if ([value isKindOfClass:[NSSet class]]) {
        // NSSets are not natively serializable, so let's just turn it into an NSArray
        transformedValue = [value allObjects];
    } else if ([value isKindOfClass:[NSOrderedSet class]]) {
        // NSOrderedSets are not natively serializable, so let's just turn it into an NSArray
        transformedValue = [value array];
    } else if (value == nil) {
        // Serialize nil values as null
        transformedValue = [NSNull null];
    } else {
        Class propertyClass = RKPropertyInspectorGetClassForPropertyAtKeyPathOfObject(mapping.sourceKeyPath, operation.sourceObject);
        if ([propertyClass isSubclassOfClass:NSClassFromString(@"__NSCFBoolean")] || [propertyClass isSubclassOfClass:NSClassFromString(@"NSCFBoolean")]) {
            transformedValue = @([value boolValue]);
        }
    }

    if (transformedValue) {
        RKLogDebug(@"Serialized %@ value at keyPath to %@ (%@)", NSStringFromClass([value class]), NSStringFromClass([transformedValue class]), value);
        [operation.destinationObject setValue:transformedValue forKeyPath:keyPath];
    }
}

It only appears that RestKit is using value transformers when value is an NSDate! Is there something that I am missing to get value transformers to work on requests?

EDIT answering Wain's questions and giving more details

This is my entity mapping code for responses. A record entity holds a collection of WTSImages:

    RKEntityMapping *imageMapping = [RKEntityMapping mappingForEntityForName:@"WTSImage" inManagedObjectStore:self.managedObjectStore];
[imageMapping addAttributeMappingsFromDictionary:@{
                                                   @"id": @"dbId",
                                                   @"status": @"status",
                                                   @"type": @"type",
                                                   @"format": @"format",
                                                   @"width": @"width",
                                                   @"height": @"height",
                                                   @"image": @"imageData"
                                                   }];
imageMapping.identificationAttributes = @[@"dbId"];
[recordMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"images" toKeyPath:@"images" withMapping:imageMapping]];

The WTSImage class is generated from CoreData and looks like this:

@interface WTSImage : NSManagedObject

@property (nonatomic, retain) NSNumber * dbId;
@property (nonatomic, retain) NSString * format;
@property (nonatomic, retain) NSNumber * height;
@property (nonatomic, retain) NSData * imageData;
@property (nonatomic, retain) NSString * status;
@property (nonatomic, retain) NSString * type;
@property (nonatomic, retain) NSNumber * width;
@property (nonatomic, retain) WTSCaptureDevice *captureDevice;
@property (nonatomic, retain) WTSRecord *record;
@property (nonatomic, retain) WTSTempImageSet *tempImageSet;

@end

I create a reverse record mapping and add a request descriptor.

RKEntityMapping *reverseRecordMapping = [recordMapping inverseMapping];
[self addRequestDescriptor:[RKRequestDescriptor requestDescriptorWithMapping:reverseRecordMapping objectClass:[WTSRecord class] rootKeyPath:@"records" method:RKRequestMethodAny]];

This is the debug log output for mapping my image object to JSON. The imageData element does not look like a normal base64 encoded string:

2014-04-10 11:02:39.537 Identify[945:60b] T restkit.object_mapping:RKMappingOperation.m:682 Mapped relationship object from keyPath 'images' to 'images'. Value: (
    {
    format = JPEG;
    height = 200;
    id = 0;
    image = <ffd8ffe0 00104a46 49460001 01000001 00010000 ffe10058 45786966 ... f77d7bf9 77b58fff d9>;
    status = C;
    type = MUGSHOT;
    width = 200;
})

And here is the POST, which my server rejects:

    2014-04-10 11:27:53.852 Identify[985:60b] T restkit.network:RKObjectRequestOperation.m:148 POST 'http://10.0.0.35:8080/Service/bs/records':
request.headers={
    Accept = "application/json";
    "Accept-Language" = "en;q=1, es;q=0.9, fr;q=0.8, de;q=0.7, ja;q=0.6, nl;q=0.5";
    "Content-Type" = "application/x-www-form-urlencoded; charset=utf-8";
    "User-Agent" = "Identify/1.0 (iPhone; iOS 7.1; Scale/2.00)";
}request.body=records[application]=Identify&records[createBy]=welcomed&records[createDt]=2014-04-10T15%3A27%3A42Z&records[description]&records[externalId]&records[groupId]=5&records[id]=0&records[images][][format]=JPEG&records[images][][height]=200&records[images][][id]=0&records[images][][image]=%3Cffd8ffe0%2000104a46%2049460001%2001000001%20000.......d773%20ffd9%3E&records[images][][status]=C&records[images][][type]=MUGSHOT&records[images][][width]=200&records[locked]&records[modifyBy]&records[modifyDt]&records[priv]
Was it helpful?

Solution 2

In RKObjectMapping classForKeyPath:, it is unable to find the class for my 'image' property. It appears that the _objectClass is a NSMutableDictionary rather than a WTSImage. This is causing the method to return a nil propertyClass

That makes sense, because the mapping destination for a request is NSMutableDictionary (and the source object is WTSImage. So, it doesn't apply any specific transformations and falls through to mappingOperation:didSetValue:forKeyPath:usingMapping: which you have already seen doesn't cater for this situation.

I think this will be hard to deal with using a transformer.

The only way I can think to deal with it right now is to add a method to WTSImage, say base64Image which returns the transformed image data and use that in your mapping (which means you won't be able to use [recordMapping inverseMapping]).

OTHER TIPS

I had exactly the same issue - NSData of image content <-> NSString BASE64 encoded for the Rest call. I got the outbound working pretty quickly as you did, but the incoming mapping was a little trickier.

I raised this issue: https://github.com/RestKit/RestKit/issues/1949 and through some working through the problem, discovered that you need to set the propertyValueClass on the RKPropertyMapping in order to get RestKit to recognize that you want to turn the NSData into a NSString. Once this is done, you get the mapping done for you.

After a deep debug at [self transformValue:value toValue:&transformedValue withPropertyMapping:attributeMapping error:&error], I find the valueTransformer I added is not at the head but the second of all valueTransformer in defaultValueTransformer.

The first valueTransformer is a RKISO8601DateFormatter, by search this key word, I find it is inserted to the head of defaultValueTransformer in [RKObjectMapping initialize].

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // Add an ISO8601DateFormatter to the transformation stack for backwards compatibility
        RKISO8601DateFormatter *dateFormatter = [RKISO8601DateFormatter defaultISO8601DateFormatter];
        [[RKValueTransformer defaultValueTransformer] insertValueTransformer:dateFormatter atIndex:0];
    });
}

The problem is that my valueTransformer is inserted before the [RKObjectMapping initialize] method get called. In another word, RKISO8601DateFormatter is inserted to the head after mine which made my valueTransformer the second one.

My solution is quite simple, just call [RKObjectMapping new] before my insert code.

BTW, you should always give a name to your valueTransformer thus it could be quickly recognized when debug.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top