Search code examples
iosobjective-cgithub-mantleyapdatabase

How to get YapDatabase and Mantle to play nicely with serialization


Suppose I have a model like this:

#import <Mantle/Mantle.h>
#import "MyCustomObject.h"
@interface MyModel : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy, readonly) NSString *UUID;
@property (nonatomic, copy) NSString *someProp;
@property (nonatomic, copy) MyCustomObject *anotherProp;
@end

#import "MyModel.h"
@implementation MyModel
+ (NSDictionary *)JSONKeyPathsByPropertyKey
{
        return @{
            @"UUID": @"id",
            @"anotherProp": NSNull.null
    };
}
}
@end

As you can see, I want to ignore anotherProp during the NSCoding serialization, as well as re-map "UUID" to "id". With YapDatabase, I do a

[transaction setObject:myModelObj forKey:@"key_1" inCollection:@"my_collection"]

but it attempts to serialize anotherProp despite my custom JSONKeyPathsByPropertyKey method, resulting in this error:

*** Caught exception encoding value for key "anotherProp" on class MyModel: -[YapDatabase encodeWithCoder:]: unrecognized selector sent to instance 0xc989630

Do I need to write a custom serializer to get YapDatabase to use JSONKeyPathsByPropertyKey?


Solution

  • Here is my current approach using an MTLModel extension to making this "just work" without a serializer or anything. If there is a serializer implementation or something that would be better, please let me know. Otherwise, this code is a life saver:

    // JSONEncodableMTLModel.h
    #import <Foundation/Foundation.h>
    #import "Mantle.h"
    
    @interface JSONEncodableMTLModel : MTLModel <MTLJSONSerializing>    
    + (NSSet*)propertyKeysToExcludeInDictionaryValue;
    @end
    
    //  JSONEncodableMTLModel.m
    #import "JSONEncodableMTLModel.h"
    #import <objc/runtime.h>
    
    @implementation JSONEncodableMTLModel
    
    +(NSDictionary *)JSONKeyPathsByPropertyKey {
        return @{};
    }
    
    + (NSSet*)propertyKeysToExcludeInDictionaryValue
    {
        return [NSSet set];
    }
    
    // this is required to ensure we don't have cyclical references when including the parent variable.
    + (NSSet *)propertyKeys {
        NSSet *cachedKeys = objc_getAssociatedObject(self, HSModelCachedPropertyKeysKey);
        if (cachedKeys != nil) return cachedKeys;
    
        NSMutableSet *keys = [NSMutableSet setWithSet:[super propertyKeys]];
    
        NSSet *exclusionKeys = [self propertyKeysToExcludeInDictionaryValue];
        NSLog(@"Caching Your Property Keys");
        [exclusionKeys enumerateObjectsUsingBlock:^(NSString *propertyKey, BOOL *stop) {
            if([keys containsObject: propertyKey])
            {
                [keys removeObject: propertyKey];
            }
        }];
    
        // It doesn't really matter if we replace another thread's work, since we do
        // it atomically and the result should be the same.
        objc_setAssociatedObject(self, HSModelCachedPropertyKeysKey, [keys copy], OBJC_ASSOCIATION_COPY);
    
        return keys;
    }
    
    @end
    

    With this code, I can explicitly set which properties do not need to be serialized by overriding propertyKeysToExcludeInDictionaryValue in any model that is a subclass. Credit goes to Stephen O'Connor.