fcanas/OHMKit

Declarative mapping between JSON and Objective-C classes


Keywords
api, ios, json, macos, mantle, objective-c, objective-c-library
License
MIT

Documentation

OHMKit

CI Status Version License Platform Coverage Status

OHMKit makes it easy to hydrate Objective-C model objects from web services or local files. It works especially well with JSON. It's a lot like Mantle and JSONModel except that OHMKit doesn't require your models to inherit from a base class, making it more suitable for use with Core Data, Parse, Realm, or other libraries that do require you to inherit from a base class.

OHMKit is a system for declaratively expressing how to translate data from JSON or plist to native Objective-C model objects. OHMKit does it without requiring your model to inherit from a base class, so it works with NSObjects, NSManagedObjects, or anything else that fits with your class hierarchy. And you can specify custom mappings anywhere you want, not just in the model. So you can keep the details of mapping a service to you models out of your model code and in your service code where it may be more appropriate.

Fit this JSON

{
  "name": "Fabian",
  "favorite_word":  "absurd",
  "favorite_number": 47
}

into this object

@interface MYModel : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *favoriteWord;
@property (nonatomic, assign) NSInteger favoriteNumber;
@end

Map user_name from your web service to userName in your Objective-C models. Map a dictionary of numbers to a UIColor. Or hydrate a whole hierarchical JSON response, including arrays, dictionaries, and arbitrarily deep hierarchies of real Objective-C objects ... with a single line of code.

Why?

OHMKit exists because RestKit (which is awesome by the way), is sometimes too big, heavy, and indirect. Because Mantle and JSONModel require your models to inherit from a base class.

Because sometimes, the web services your code consumes doesn't perfectly match your model objects.

OHMKit is under 200 lines of well-tested code being leveraged in the app store now in apps used by millions of users.

What OHMKit is Not

OHMKit doesn't know about networks. Use AFNetworking.

OHMKit doesn't know about routes. Use SOCKit.

OHMKit doesn't know about JSON. Use NSJSONSerialization

OHMKit doesn't know about CoreData. It will not manage graphs of entities for you quite like RestKit does. But OHMKit does not care about your model class' super class. So you can safely make subclasses of NSManagedObject mappable.

Usage

Basic Mapping

Given a model

@interface MYModel : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *favoriteWord;
@property (nonatomic, assign) NSInteger favoriteNumber;
@end

Anywhere in you application, make the model mappable, and assign it a dictionary of mappings from the keys a service will provide to the keys your actual model object uses.

OHMMappable([MYModel class]);
OHMSetMapping([MYModel class], @{@"favorite_word"  : @"favoriteWord",
                                 @"favorite_number": @"favoriteNumber");

And now anywhere in your application, objects of the class MYModel can be hydrated with a dictionary from a service whose keys will be translated by the mapping dictionary you provided.

MYModel *testModel = [[MYModel alloc] init];

[testModel setValuesForKeysWithDictionary:@{@"name"           : @"Fabian",
                                            @"favorite_word"  : @"absurd",
                                            @"favorite_number": @47];

Likewise, anywhere in your application, objects of the class MYModel can be dehydrated to a dictionary for use with a service whose keys will be translated by the mapping dictionary you provided.

MYModel *testModel = [[MYModel alloc] init];
testModel.name = @"Fabian";
testModel.favoriteWord = @"absurd";
testModel.favoriteNumber = 47;

NSDictionary *dictionary = [testModel dictionaryWithValuesForKeys:OHMMappableKeys([MYModel class])];

Recursive Mapping

Recursive mapping of mappable objects comes for free. If an object conforming to <OMMappable> has a property whose type also conforms to <OMMappable>, and the value for that key in the hydration dictionary is itself a dictionary, we'll instantiate a new model object and hydrate it.

@interface MYClass : NSObject
@property (nonatomic, strong) NSString *name;
@end

@interface MYClass2 : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *favoriteWord;
@property (nonatomic, assign) NSInteger favoriteNumber;
@property (nonatomic, assign) MYClass *favoriteObject;
@end

OHMMappable([MYClass class]);

OHMMappable([MYClass2 class])
OHMSetMapping([MYClass2 class], @{@"favorite_word"  : @"favoriteWord", 
                                @"favorite_number": @"favoriteNumber", 
                                @"favorite_object" : @"favoriteObject"});

MYModel *testModel = [[MYClass2 alloc] init];

NSDictionary *class2Response = @{@"name"           : @"Fabian", 
                                 @"favorite_word"  : @"absurd", 
                                 @"favorite_number": @2, 
                                 @"favorite_object": @{@"name" : @"Rock"}};

[testModel setValuesForKeysWithDictionary:class2Response];

Now, testModel.favoriteObject is an instance of MYClass hydrated with "Rock" as its name.

Internally, the new model object is initialized with [[ alloc] init], and then hydrated with [ setValuesForKeysWithDictionary:dictionary]. If you have a model that needs special consideration for initialization, use an adapter block.

Arrays

Arrays of dictionaries can be mapped to a class as well.

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

@interface Roster : NSObject
@property (nonatomic, strong) NSArray *people;
@end

OHMMappable([Person class]);
OHMSetArrayClasses([Roster class], @{@"people":[Person class]});

NSDictionary *response = @{@[@{@"name":@"Bert"},
                             @{@"name":@"Ernie"},
                             @{@"name":@"Count"}];

Roster *roster = [Roster new];
[roster setValuesForKeysWithDictionary:response];

Blocks serve as adapters to handle special properties

Users can pass a dictionary of blocks for field requiring special handling. Say a service sends back a dictionary that looks something like this:

{
    "favorite_color": [
        122,
        50,
        80
    ],
    "least_favorite_color": [
        121,
        51,
        81
    ]
}

and we expect to map it to a model like this

@interface MYModel : NSObject
@property (nonatomic, strong) UIColor *favoriteColor;
@property (nonatomic, strong) UIColor *leastFavoriteColor;
@end

You can adapt the response with an adapter block:

OHMMappable([MYModel class]);
OHMSetMapping([MYModel class], @"least_favorite_color" : @"leastFavoriteColor", @"favorite_color" : @"favoriteColor")
OHMValueAdapterBlock colorFromNumberArray = ^(NSArray *numberArray) {
    return [UIColor colorWithRed:[numberArray[0] integerValue]/255.0
                           green:[numberArray[1] integerValue]/255.0
                            blue:[numberArray[2] integerValue]/255.0
                           alpha:1];
};
OHMSetAdapter([MYModel class], @{@"favoriteColor": colorFromNumberArray, @"leastFavoriteColor": colorFromNumberArray});

When dehydrating, you can adapt the output with a reverse adapter block:

OHMMappable([MYModel class]);
OHMSetMapping([MYModel class], @"least_favorite_color" : @"leastFavoriteColor", @"favorite_color" : @"favoriteColor")
OHMValueAdapterBlock numberArrayFromColor = ^(NSColor *color) {
    CGFloat red, green, blue, alpha;
    [color getRed: &red green: &green blue: &blue alpha: &alpha];
    return @[@(red), @(green), @(blue)];
};
OHMSetReverseAdapter([MYModel class], @{@"favoriteColor": numberArrayFromColor, @"leastFavoriteColor": colorFromNumberArray});

Note that the key for the adapter is the key on the model object, not on the response. And adapters are added for a property, not a type. If the above example had multiple properties that were colors, you would have to set an adapter block for each property. It would be smart to reuse adapter blocks in your code.

The OHMValueAdapterBlock type is a block that takes an id and returns an id. i.e typedef id(^OHMValueAdapterBlock)(id);

Using it in a project

Use CocoaPods, add OHMKit to your PodFile, and run $ pod install

pod 'OHMKit'

How?

OHMKit is a mixin that makes it easy to keep any direct knowledge of the idiosyncrasies of the service you're consuming tucked away in a single place.

It leverages the power of Key Value Coding (KVC) that's built right in to Cocoa. It safely wraps -setValue:forKey: and -setValue:forUndefinedKey: to make calls to setValuesForKeysWithDictionary: and dictionaryWithValuesForKeys: extremely powerful.

Contributing

Bug fixes, pull requests, enhancement requests and feedback are welcome.

If you plan on contributing code, please notice that OHMKit has tests. If you're fixing a bug, please include a test that exposes the bug and therefore guards against a regression.

TODO

Undefined Keys

The behavior of undefined keys should be configurable at 3 levels:

  1. Raise, because I should know about everything.
  2. Drop unrecognized keys. We don't need them, but we shouldn't crash.
  3. Add keys to a dictionary so that serialization/deserialization can be symmetric

Option 2 is currently the only behavior, and I'm inclined to leave is as the default behavior.

NSCoding

It might be nice if we built a way to make a class NSCoding compatible if it's not already. I like Mantle, but I don't want to be told what my super class should be.

NSValueTransformer

Adapter blocks versus NSValueTransformers. There's no reason why both can't co-exist.

License

Copyright (c) 2013-2015 Fabian Canas. All rights reserved.

This code is distributed under the terms and conditions of the MIT license.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.