RestKit Request Value Transformer? - ios

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]

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.

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]).

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.

Related

Trouble filtering array of custom objects by an NSNumber using NSPredicate

This should be straightforward but something is preventing me from filtering an array of custom objects by NSNumber using NSPredicate. Perhaps it has something to do with the datatype when converting from JSON but I can't figure it out.
I download data from a JSON in an array of custom Objects that look like:
{"hid":"47","public":"1"}
The code to parse the JSON looks like:
if (feedElement[#"public"] && ![feedElement[#"public"] isEqual:[NSNull null]]) {
newMyObject.pub = feedElement[#"public"]== nil ? #"0" : feedElement[#"public"];}
The object looks like:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#interface MyObject : NSObject
#property (nonatomic, retain) NSNumber * hid;
#property (nonatomic, retain) NSNumber * pub;
#end
NS_ASSUME_NONNULL_END
The objects are placed in an NSArray * myObjects
My NSPredicate and filter code looks like:
NSPredicate *pubPred = [NSPredicate predicateWithFormat:#"pub == 1"];
NSArray *filteredArray = [myObjects filteredArrayUsingPredicate:pubPred];
When I log [myObjects valueForKey:#"pub"], it logs as 1,1,1, etc. so I know that the values for pub are all 1 however the resulting filteredArray is empty.
What could be wrong with my code?
Thanks for any suggestions.
Edit: I changed public to pub in the object in case public was a reserved word but it did not change anything
With sample code : {"hid":"47","public":"1"}
newMyObject.pub = feedElement[#"public"]== nil ? #"0" : feedElement[#"public"];
In case of public value is present in JSON, you'll set pub property with feedElement[#"public"], which means, it will be #"1" (with the sample), which means you'll put a NSString.
In case of public value is present in JSON, you'll set pub property with #"0" which means you'll put a NSString.
It doesn't matter if it's declared #property (nonatomic, retain) NSNumber * pub;, you are setting a NSString, not a NSNumber.
Want some code testing?
#interface MyObject : NSObject
#property (nonatomic, retain) NSNumber *hid;
#property (nonatomic, retain) NSNumber *pub;
+(void)test;
#end
And
#implementation MyObject
-(id)initWithPub:(NSNumber *)pub andHID:(NSNumber *)hid {
self = [super init];
if (self) {
self.pub = pub;
self.hid = hid;
}
return self;
}
-(NSString *)description {
return [NSString stringWithFormat:#"%# pub: %# - hid: %#", [super description], [self pub], [self hid]];
}
+(NSArray *)arrayList {
return #[[[MyObject alloc] initWithPub:#1 andHID:#3], //Here with real NSNUmbner
[[MyObject alloc] initWithPub:#"1" andHID:#"2"]]; //Here with NSString instead
}
+(void)test {
NSArray *list = [MyObject arrayList];
NSPredicate *predicateNSNumber = [NSPredicate predicateWithFormat:#"pub == %#", #1];
NSArray *filteredNSNumber = [list filteredArrayUsingPredicate:predicateNSNumber];
NSLog(#"Filtered-NSNumber: %#", filteredNSNumber);
NSPredicate *predicateNSString = [NSPredicate predicateWithFormat:#"pub == %#", #"1"];
NSArray *filteredNSString = [list filteredArrayUsingPredicate:predicateNSString];
NSLog(#"Filtered-NSString: %#", filteredNSString);
}
Output:
$> Filtered-NSNumber: (
"<MyObject: 0x60000024cba0> pub: 1 - hid: 3"
)
$> Filtered-NSString: (
"<MyObject: 0x60000024d1e0> pub: 1 - hid: 2"
)
You can state: "Yeah, but you have a warning in [[MyObject alloc] initWithPub:#"1" andHID:#"2"]: Incompatible pointer types sending 'NSString *' to parameter of type 'NSNumber *', yes, I have. You should have it too.
In fact, you'll have it too if you wrote your ternary if :
if (feedElement[#"public"] == nil) {
newMyObject.pub = #"0"; //Incompatible pointer types assigning to 'NSNumber * _Nonnull' from 'NSString *'
} else {
newMyObject.pub = feedElement[#"public"]; //Here, no warning, because it's hope you know what you are doing and setting a NSNumber here.
}
What about using this to your code to check:
for (MyObject *anObject in list) {
NSLog(#"Testing: %#", anObject);
if ([[anObject pub] isKindOfClass:[NSString class]]) {
NSLog(#"Pub is a String");
} else if ([[anObject pub] isKindOfClass:[NSNumber class]]) {
NSLog(#"Pub is a NSNumber");
}
NSLog(#"Attempt to call -stringValue which is a NSNumber method, not a NSString one");
NSLog(#"Attempt Call: %#\n", [[anObject pub] stringValue]);
}
You should get a -[__NSCFConstantString stringValue]: unrecognized selector sent to instance error, because it's really a NSString, not a NSNumber.
Solutions:
You need to fix your parsing, or change the type of property in MyObject.
With keeping the property as a NSNumber:
if ([feedElement[#"public"] isKindOfClass:[NSString class]]) {
self.pub = #([feedElement[#"public"] integerValue]); //Transform it into a NSInteger, then into a NSNumber with #(someInteger)
} else ([feedElement[#"public"] isKindOfClass:[NSNumber class]]) {
self.pub = feedElement[#"public"]; //It's already a NSNumber instance
} else {
self.pub = #(0); //Default value because unknown class or not present
}

RestKit custom RKValueTransformer not being run for NSDate property

I would like to run a custom RKValueTransformer on an NSDate property when creating a specific request. I'm happy (and would prefer to) use the default in all other scenarios. My understanding is that I can set up the mapping, specify a value transformer and it'll use that, only falling back to the defaults if no value transformer is set. I'm running RestKit 0.23.3.
I have the following mapping set up for my request (I've obviously trimmed a bunch from this):
#property (nonatomic, retain) NSDate * dueDate;
...
+(RKEntityMapping *) createRequestMapping
{
static RKEntityMapping* map = nil;
if (map == nil)
{
map = (RKEntityMapping *)[RKObjectMapping requestMapping];
[map addAttributeMappingsFromDictionary:...];
RKValueTransformer *dueDateValueTransformer = [RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class inputValueClass, __unsafe_unretained Class outputValueClass) {
return [inputValueClass isSubclassOfClass:[NSDate class]];
} transformationBlock:^BOOL(id inputValue, __autoreleasing id *outputValue, __unsafe_unretained Class outputClass, NSError *__autoreleasing *error) {
*outputValue = [[self dueDateRequestFormatter] stringFromDate:inputValue];
return YES;
}];
RKAttributeMapping *dueDateMapping = [RKAttributeMapping attributeMappingFromKeyPath:#"dueDate" toKeyPath:#"due_date"];
dueDateMapping.valueTransformer = dueDateValueTransformer;
[map addPropertyMapping:dueDateMapping];
}
return map;
}
When my test for this runs, I can see that the mapping is used (since the target key path "due_date" exists in the dictionary), but the value transformer is not - rather, it appears the default NSDate transformer is used:
XCTAssertEqualObjects(#"2014-05-01", [item objectForKey:#"due_date"]);
-[RestKitCreateMappingTests testCreateSequenceMappings] failed: ((#"2014-05-01") equal to ([item objectForKey:#"due_date"])) failed: ("2014-05-01") is not equal to ("2014-05-01 00:00:00 +0000")
I've set breakpoints in both the validation and transformation blocks, and neither is hit.
Why is RestKit not using my value transformer?
I had to specify the propertyValueClass so that RestKit knew what type of target property to map to:
RKAttributeMapping *dueDateMapping = [RKAttributeMapping attributeMappingFromKeyPath:#"dueDate" toKeyPath:#"due_date"];
dueDateMapping.valueTransformer = dueDateValueTransformer;
dueDateMapping.propertyValueClass = [NSString class];
[map addPropertyMapping:dueDateMapping];
Without this, the following check failed in the mapping operation, and it never got a chance to use the custom value transformer, rather it just outputs the input value:
Class transformedValueClass = propertyMapping.propertyValueClass ?: [self.objectMapping classForKeyPath:propertyMapping.destinationKeyPath];
if (! transformedValueClass) {
*outputValue = inputValue;
return YES;
}
BOOL success = [propertyMapping.valueTransformer transformValue:inputValue toValue:outputValue ofClass:transformedValueClass error:error];
return success;

Message sent to deallocated instance with ARC using custom getter and setter

I'm trying to implement a custom getter and setter for my custom object HFObject and my app crashed with a Message sent to deallocated instance error despite using ARC.
I've read every single related post, the ones that were written pre-ARC don't apply, and everything else didn't help. I have the zombie object debugger option turned on.
Setting up the custom HObject
Within HObject.h I have declared these four properties:
#property (retain) NSString *email; //Will use custom getter/setter
#property (retain) NSString *firstName; //Will use custom getter/setter
#property (retain) NSDate *date; //Will use custom getter/setter
#property (nonatomic, retain) NSMutableDictionary *values;
In the implementation of HObject, I have removed the automatic getting and setting of email. firstName, and date by utilizing #dynamic like so
#dynamic email;
#dynamic firstName;
#dynamic date;
I also allocate the values dictionary in my HObject init
- (id)init {
self = [super init];
if (self) {
self.values = [NSMutableDictionary dictionary];
}
return self;
}
Implementing Custom Getter & Sender
For my custom getter/setter. I have overridden
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
and
- (void)forwardInvocation:(NSInvocation *)invocation
As shown below:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
NSString *sel = NSStringFromSelector(selector);
if ([sel rangeOfString:#"set"].location == 0) {
return [NSMethodSignature signatureWithObjCTypes:"v#:#"];
} else {
return [NSMethodSignature signatureWithObjCTypes:"##:"];
}
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
NSString *key = NSStringFromSelector([invocation selector]);
if ([key rangeOfString:#"set"].location == 0) {
key = [key substringWithRange:NSMakeRange(3, [key length]-4)];
id obj;
[invocation getArgument:&obj atIndex:2];
[self.values setObject:obj forKey:key];
} else {
id obj = [self.values objectForKey:key];
[invocation setReturnValue:&obj];
}
}
What I'm trying to do here is store all of the values of the property into my values dictionary and retrieve them from there as well.
App Crashing
In the implementation of my view controller, I try to create a HObject and set values for my properties, and then I log the values dictionary to see all of my values.
- (void)buttonPressed:(id)sender {
HObject *obj = [[HObject alloc] init];
NSString *name = #"this is a string object";
obj.date = [NSDate date];
obj.email = #"email#website.com";
obj.firstName = [NSString stringWithFormat:#"%#", name];
NSLog(#"Values %#", [obj values]);
}
At that point the app crashes and this is my console log
2014-07-27 04:12:37.899 App[61501:60b] Values {
Date = "2014-07-27 08:12:37 +0000";
Email = "email#website.com";
FirstName = "this is a string object";
}
2014-07-27 04:12:37.901 HeadsUp[61501:60b] *** -[CFString release]: message sent to deallocated instance 0x109473fe0
If you can help me from here, I would greatly appreciate it. I am also including my debugging process in case that will help you
My debugging Process (Long, skip if you can already help me)
I originally created many of these objects and stored them in an array, and when I do that, as opposed to creating a single object. my app crashed a bit different.
My array:
#property (nonatomic, strong) NSArray *array;
Methods:
- (void)createArray
{
int i = 1; //number of testobjs
NSMutableArray *objects = [NSMutableArray arrayWithCapacity:i];
for (int j = 0; j<i; j++) {
HFObject *obj = [[User alloc] init];
NSString *name = #"this is a string object";
[obj setObject:[NSDate date] forKey:#"Date"];
obj.email = #"email#website.com";
obj.firstName = [NSString stringWithFormat:#"%#", name];
[objects addObject:obj];
}
self.array = [NSArray arrayWithArray:objects];
}
- (void)buttonPressed:(id)sender {
HObject *object = [self.array objectAtIndex:0];
NSLog(#"Values %#", [object values]);
}
Crash log:
2014-07-27 04:34:02.893 App[61623:60b] *** -[CFString isNSString__]: message sent to deallocated instance 0x1094988f0
(lldb)
Now this crash log is almost the same as the one before, except this didn't log the values inside of [object values]
Investigating the issue a bit, I looked at the left window (not sure what it is actually called) of the debugger and I saw this:
(Treat HFObject as HObject and dirtyValues as values; I renamed them for presentational purposes)
You can see that under the key #"FirstName" there is no value.
I did several similar tests where I changed the values of the properties I was setting and changed the data types. More often than not, not only did FirstName not have a value, neither did Date. However, the value of email was always present.
After researching about dataTypes, I realized it was because email was a string literal which can't deallocated. On the other hand firstName and date were objects, which can be deallocated.
The crash log refers to a CFString property, which I learned doesn't use ARC. I never created a Core Foundation object, so I set out to found out it was being created in setter by logging the [obj class]:
- (void)forwardInvocation:(NSInvocation *)invocation
{
NSString *key = NSStringFromSelector([invocation selector]);
if ([key rangeOfString:#"set"].location == 0) {
key = [key substringWithRange:NSMakeRange(3, [key length]-4)];
id obj;
[invocation getArgument:&obj atIndex:2];
NSLog(#"%#", [obj class]); //I ADDED THIS LOG
[self.values setObject:obj forKey:key];
} else {
id obj = [self.values objectForKey:key];
[invocation setReturnValue:&obj];
}
}
After crashing one more time, I got the obj classes
2014-07-27 04:58:03.893 HeadsUp[61765:60b] __NSDate
2014-07-27 04:58:03.894 HeadsUp[61765:60b] __NSCFConstantString
2014-07-27 04:58:03.894 HeadsUp[61765:60b] __NSCFString
2014-07-27 04:58:03.904 HeadsUp[61765:60b] *** -[__NSDate release]: message sent to deallocated instance 0x109554370
(lldb)
Here you can see Date is being deallocated for some reason, and my string are now __NSCF strings.
I tried resetting the strings to NSStrings using (__brigde NSString *)obj and every other possible way you can bridge a CF object to ARC, however that didn't work either.
Here is everything I've done. I appreciate any and all help.
The problem is here:
id obj;
[invocation getArgument:&obj atIndex:2];
getArgument simply copies the object pointer into obj without retaining it.
However, since obj is (by default) a __strong variable, it will be released at
the end of the current method. To solve the problem, use
__unsafe_unretained id obj;
[invocation getArgument:&obj atIndex:2];
Note also that your getter implementation does not work. For example, setFirstName:
stores the key in the dictionary using the key "FirstName", but the getter firstName
tries to read the value for the key "firstName".
(As already mentioned in a comment, it would probably easier and less error-prone
to just override the accessor methods for the three properties separately, instead
of dynamic forwarding.)

How to omit null values in JSON Dictionary using Mantle?

I have MyModel inheriting from MTLModel (using the GitHub Mantle pod).
MyModel.h
#import <Mantle/Mantle.h>
#interface MyModel : MTLModel <MTLJSONSerializing>
#property (nonatomic, copy, readonly) NSString *UUID;
#property (nonatomic, copy) NSString *someProp;
#property (nonatomic, copy) NSString *anotherProp;
#end
MyModel.m
#import "MyModel.h"
#implementation MyModel
+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
return #{
#"UUID": #"id",
#"someProp": #"some_prop",
#"anotherProp": #"another"
};
}
}
#end
Now I want to send the JSON to the backend using AFNetworking. Before that I convert the model instance to a JSON NSDictionary to use as parameters/body payload within my request.
NSDictionary *JSON = [MTLJSONAdapter JSONDictionaryFromModel:myModel];
But this JSON consists of strange "" Strings for properties of my model that are nil. What i instead want is Mantle to omit these key/value pairs and just spit out a JSON with only the properties that are not nil or NSNull.null, whatever.
This is a common issue with Mantle and it's called implicit JSON mapping.
MTLJSONAdapter reads all properties of a model to create a JSON string optionally replacing property names with ones given in +JSONKeyPathsByPropertyKey.
If you want some properties to be excluded from the JSON representation of your model, map them to NSNull.null in your +JSONKeyPathsByPropertyKey:
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{
#"UUID": #"id",
#"someProp": #"some_prop",
#"anotherProp": #"another",
#"myInternalProperty": NSNull.null,
#"myAnotherInternalProperty": NSNull.null,
};
}
The implicit JSON mapping has lately become a noticeable problem, a solution for which is currently being discussed at Mantle's home repository at GitHub.
See issues #137, #138, #143 and the current discussion under #149.
EDIT: I clearly misunderstood the question, but now, when I suppose I understand it correctly, the answer is simple.
MTLJSONAdapter generates the JSON data using MTLModel's dictionaryValue property. If you wish to exclude a property from the JSON itself, you can overwrite that method in your MYModel:
- (NSDictionary *)dictionaryValue {
NSMutableDictionary *originalDictionaryValue = [[super dictionaryValue] mutableCopy];
if (self.aPropertyThatShouldBeExcludedWhenNil == nil) {
[originalDictionaryValue removeObjectForKey:#"aPropertyThatShouldBeExcludedWhenNil"];
}
/* repeat the process for other "hidden" properties */
return originalDictionaryValue;
}
EDIT #2: Check out the code* for removing all values that are nil:
- (NSDictionary *)dictionaryValue {
NSMutableDictionary *modifiedDictionaryValue = [[super dictionaryValue] mutableCopy];
for (NSString *originalKey in [super dictionaryValue]) {
if ([self valueForKey:originalKey] == nil) {
[modifiedDictionaryValue removeObjectForKey:originalKey];
}
}
return [modifiedDictionaryValue copy];
}
* - code sample suggested by matths.
I remove nil valued keys by creating an MTLJSONAdapter subclass, and overriding -serializablePropertyKeys:forModel: method.
MTLJSONAdapterWithoutNil.h
/** A MTLJSONAdapter subclass that removes model dictionaryValue keys whose value is `[NSNull null]`. */
#interface MTLJSONAdapterWithoutNil : MTLJSONAdapter
#end
MTLJSONAdapterWithoutNil.m
#import "MTLJSONAdapterWithoutNil.h"
#implementation MTLJSONAdapterWithoutNil
- (NSSet *)serializablePropertyKeys:(NSSet *)propertyKeys forModel:(id<MTLJSONSerializing>)model {
NSMutableSet *ms = propertyKeys.mutableCopy;
NSDictionary *modelDictValue = [model dictionaryValue];
for (NSString *key in ms) {
id val = [modelDictValue valueForKey:key];
if ([[NSNull null] isEqual:val]) { // MTLModel -dictionaryValue nil value is represented by NSNull
[ms removeObject:key];
}
}
return [NSSet setWithSet:ms];
}
#end
And use this to create JSON dictionary instead. Like this:
NSDictionary *JSONDictionary = [MTLJSONAdapterWithoutNil JSONDictionaryFromModel:collection error:nil];
NOTE: if you are overriding NSValueTransformer methods for array or dictionary properties, you also have to change the MTLJSONAdapter class to your subclass as well. Like this:
+ (NSValueTransformer *)myDailyDataArrayJSONTransformer {
return [MTLJSONAdapterWithoutNil arrayTransformerWithModelClass:KBDailyData.class];
}
Overriding - dictionaryValues did not give me the expected behavior
So I created a method for MTL Base class
- (NSDictionary *)nonNullDictionaryWithAdditionalParams:(NSDictionary *)params error:(NSError *)error {
NSDictionary *allParams = [MTLJSONAdapter JSONDictionaryFromModel:self error: &error];
NSMutableDictionary *modifiedDictionaryValue = [allParams mutableCopy];
for (NSString *originalKey in allParams) {
if ([allParams objectForKey:originalKey] == NSNull.null) {
[modifiedDictionaryValue removeObjectForKey:originalKey];
}
}
[modifiedDictionaryValue addEntriesFromDictionary:params];
return [modifiedDictionaryValue copy];
}
The EDIT #2 used to work for me with the previous Mantle code base. Now I have to do the following to continue using EDIT #2:
In file MTLJSONAdapter.m, replace this line:
NSDictionary *dictionaryValue = [model.dictionaryValue dictionaryWithValuesForKeys:propertyKeysToSerialize.allObjects];
with
NSDictionary *dictionaryValue = model.dictionaryValue;
The above is my current workaround to get
{ }
instead of
{
"AddressLine2" : null,
"City" : null,
"ZipCode" : null,
"State" : null,
"AddressLine1" : null
}

passing base64 image data to ios app for consumption using RESTkit

please help so here is the problem i got a rails app that sends a json image data base64. The json sends but when i try to access the data value in ios app it comes back as null. Here is my rails code,code
{
"profile_pic": "<%= Base64.encode64(File.read('/Users/rui_y/connect_Me2/public'+
#user.avatar_photo_url(:thumb)).gsub("\n", '')) %>",
}
the json comes back as
[{"profile_pic": "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcH......
....... many more lines etc etc...................................................
+p/Ob9Xrf6D8po/hOhI4U5SSm4Es8vUE/riYmJj2B0iefbkz/2Q==" }]
the Restkit is mapped like this
[userMapping mapKeyPath:#"profile_pic" toAttribute:#"profilePic"];
NSLog(#"people id %#",[[objects objectAtIndex:0]profilePic ]);
//and it comes back as null
2012-02-09 20:08:48.073 ConnecTest[78232:207] profile_pic (null)
all the other values when i nslog is accessable so im wondering how to map this data.
You can map this as you do for all strings.
I take this in my code, but I map a list of key/value object :
Mapping :
listMapping = [RKObjectMapping mappingForClass:[VOKeyValue class]];
[listMapping mapKeyPath:#"value" toAttribute:#"value"];
[listMapping mapKeyPath:#"key" toAttribute:#"key"];
[[RKObjectManager sharedManager].mappingProvider setMapping:listMapping forKeyPath:#"list"];
Value Object :
#interface VOKeyValue : NSObject
{
NSString * key;
NSString * value;
}
#property (nonatomic, retain) NSString * key;
#property (nonatomic, retain) NSString * value;
#end
Request :
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"yourResourceURL..." delegate:self];
In my CallBack function :
VOKeyValue*img=[objects objectAtIndex:0];
if (![img.key isEqualToString:#"image"]) {
img=[objects objectAtIndex:1];
}
Finally I can map this Json :
{"list":[{"key":"image","value":"/9j/4AATSkZJRgABAgIAAAAAAAAAAAD/wAARCADwAUADASEAAhEBAxEB/....YFJCPAP/ZAAA="},{"key":"other","value":"xxxx..."}]}
I hope my exemple can help you.

Resources