18

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.

David Snabel-Caunt
  • 57,804
  • 13
  • 114
  • 132
matths
  • 760
  • 9
  • 19
  • Can you post the full code for MyModel? – David Snabel-Caunt Sep 25 '13 at 13:50
  • I edited the question. So imagine an API which is not fully RESTful. So when fetching the data of that model, "some_prop" would be not delivered, which results in being nil. When converting this model back into JSON, the property is translated into "some_prop": "" :( – matths Sep 26 '13 at 18:20

4 Answers4

39

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.

akashivskyy
  • 44,342
  • 16
  • 106
  • 116
  • Thanks for that answer. And I'm already aware of this kind of exclusion of some properties from the JSON representation of the model. But this is not a solution for just omitting null values _at runtime_ when they are null, but including them, if they have valid values. – matths Oct 09 '13 at 09:23
  • OK, It seems like i misunderstood the question. I edited my answer and I hope that this time it will fix your issue. ;) – akashivskyy Oct 16 '13 at 17:40
  • That sounds interesting and indeed like a solution. I'll try tomorrow morning (CET) in the office from my working machine, and accept the answer when it works. – matths Oct 17 '13 at 18:32
  • ...and what's the status? – akashivskyy Oct 20 '13 at 21:02
  • it works. I might edit your answer and add my code for nil checking all of the properties. – matths Oct 22 '13 at 12:41
  • 1
    Glad that it's working! I added the code sample for checking all properties, though I improved it a little bit. ;) – akashivskyy Oct 22 '13 at 12:54
  • 2
    Strangely the EDIT #2 solution is not working for me. The dictionaryValue method appears to never be called. I had to use value transformers for every value that could be null. – Rhuantavan Jul 29 '14 at 08:14
  • 1
    EDIT #2 used to work for me. But now with the latest Mantle it doesn't work. I see something like "Address = "";" instead of nothing. Anyone has an updated solution? thx! – Golden Thumb May 28 '15 at 21:52
  • Yeah, EDIT #2 doesn't work for me either. I dig into **MTLJSONAdapter.m** and found `NSDictionary *dictionaryValue = [model.dictionaryValue dictionaryWithValuesForKeys:propertyKeysToSerialize.allObjects];`. The variable `model.dictionaryValue` has nil-value keys removed correctly but it got inserted back with `- dictionaryWithValuesForKeys`. May be we have to manage the `propertyKeysToSerialize` as well. That's just a clue. I will update if i have a good solution. – Hlung Jun 29 '15 at 05:30
  • Done, see my answer below http://stackoverflow.com/a/31110765/467588 . It's probably not the best solution, but it works for me :) – Hlung Jun 29 '15 at 09:23
  • This doesn't work anymore, need to subclass MTLJSONAdapter – Daniel Galasko Jul 27 '15 at 16:26
  • overriding ```- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;``` did the job for me – Dannie P Aug 04 '15 at 12:55
2

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];
}
Hlung
  • 13,850
  • 6
  • 71
  • 90
  • What is the `val` in `[[NSNull null] isEqual:val]`? – Elliot Schrock Jul 14 '15 at 19:06
  • 1
    looks like you forgot to say `val = modelDictValue[key]` – Daniel Galasko Jul 27 '15 at 11:43
  • Yes, I forgot that, sorry T_T. Edited. – Hlung Jul 27 '15 at 13:44
  • Hi @Hlung did you succeed making it working for nested models? – Sergii N. Sep 21 '15 at 12:33
  • @SergiiN. I didn't tried that case. Is this solution not working? – Hlung Sep 22 '15 at 08:28
  • @Hlung it work for top level model, but for its nested models default `MTLJSONAdapter` is still used based on `Mantle` implementation here: https://github.com/Mantle/Mantle/blob/master/Mantle/MTLJSONAdapter.m#L106 **UPD:** I invetsigated more and for nested class `NSValueTransformers` are being used based on this method: https://github.com/Mantle/Mantle/blob/master/Mantle/MTLJSONAdapter.m#L475 And they are all using creating `MTLJSONAdapter ` instance internally :( – Sergii N. Sep 22 '15 at 08:47
  • Thanks for the solution. The iteration should be on `propertyKeys` however, to avoid mutating a collection under iteration. – eli Jun 10 '16 at 13:36
0

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];
    }
Igor Kovryzhkin
  • 2,195
  • 1
  • 27
  • 22
-1

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
}
Golden Thumb
  • 2,531
  • 21
  • 20