Objective Cat

A journal of discoveries during iOS and Mac development.

Mantle

01 Feb 2014 by Alexander Schuch

Mantle is a model framework for iOS that provides a convenient way to create objects from JSON and transform those objects back into JSON. This is especially useful when dealing with a remote API.

We are going to take a look at MTLModel, MTLJSONAdapter and why you might want to consider using Mantle for your next project.

MTLModel

MTLModel provides an easy way to map NSDictionary objects to Objective-C classes and vice-versa.

To get started, lets look at a simple example. Assume we get the following JSON response from our API endpoint and want to create and populate our (yet to be created) CATProfile model class.

{
  "id": 1,
  "name": "Objective Cat",
  "birthday": "2013-09-12 13:29:36 +0100",
  "website": "http://objc.at",
  "location": { "lat": "48.2083", "lon": "16.3731" },
  "relationship_status": "single",
  "awesome": true
}

Lets create a new MTLModel subclass that represents the JSON object above.

// CATProfile.h
typedef NS_ENUM(NSInteger, CATRelationshipStatus) {
    CATRelationshipStatusSingle = 0,
    CATRelationshipStatusInRelationship,
    CATRelationshipStatusComplicated
};

@interface CATProfile : MTLModel<MTLJSONSerializing>

@property(strong, nonatomic) NSNumber *profileId;
@property(strong, nonatomic) NSString *name;
@property(strong, nonatomic) NSDate *birthday;
@property(strong, nonatomic) NSURL *website;
@property(nonatomic) CLLocationCoordinate2D locationCoordinate;
@property(nonatomic) CATRelationshipStatus relationshipStatus;
@property(nonatomic, getter=isAwesome) BOOL awesome;

@end

The CATProfile class inherits from MTLModel and implements the MTLJSONSerializing protocol. The protocol requires us to implement +JSONKeyPathsByPropertyKey.

// CATProfile.m
@implementation

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    // properties defined in header < : > key in JSON Dictionary
    return @{
             @"profileId":          @"id",
             @"websiteURL":         @"website",
             @"locationCoordinate": @"location",
             @"relationshipStatus": @"relationship_status",
            };
}

@end

+JSONKeyPathsByPropertyKey returns an NSDictionary with key-value pairs of each model property that should be matched to a value in JSON. This makes sure Mantle knows which JSON key to use to populate a specific model property.

Obiously absent from this list are the name, birthday and awesome properties. If a property is ommited from the dictionary, Mantle will automatically look for a JSON key with the same name as the property defined in the header.

NSValueTransformer

Mantle can handle the conversion of arbitrary types such as NSString and NSNumber by default. However, it needs some help with non-arbitrary types such as NSURL and enums as well as custom structs like CLLocationCoordinate2D.

Mantle relies on the help of Foundation's NSValueTransformer to map values between the JSON representation of the model to the actual properties on the Objective-C object.

To create a custom transformer for a model property, we need to implement a class method called +<propertyName>JSONTransformer and return the desired NSValueTransformer.

// mapping birthday to NSDate and vice-versa
+ (NSValueTransformer *)birthdayJSONTransformer {
    return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *dateString) {
        return [self.dateFormatter dateFromString:dateString];
    } reverseBlock:^(NSDate *date) {
        return [self.dateFormatter stringFromDate:date];
    }];
}

+ (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

    return dateFormatter;
}

Mantle calls this method at runtime to determine how to transform the birthday property. The forward block transforms a string to an NSDate object and the reverse block takes the NSDate object and converts it back to a string. Nice!


For reference, here is a list of transformer methods for all the other non-arbitrary properties that need our attention.

NSURL ↔︎ JSON string
+ (NSValueTransformer *)websiteURLJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
CLLocationCoordinate2D ↔︎ JSON object
+ (NSValueTransformer *)locationCoordinateJSONTransformer {
    return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSDictionary *coordinateDict) {
        CLLocationDegrees latitude = [coordinateDict[@"lat"] doubleValue];
        CLLocationDegrees longitude = [coordinateDict[@"lon"] doubleValue];
        return [NSValue valueWithMKCoordinate:CLLocationCoordinate2DMake(latitude, longitude)];
    } reverseBlock:^(NSValue *coordinateValue) {
        CLLocationCoordinate2D coordinate = [coordinateValue MKCoordinateValue];
        return @{@"lat": @(coordinate.latitude), @"lon": @(coordinate.longitude)};
    }];
}
enum ↔︎ JSON string
+ (NSValueTransformer *)relationshipStatusJSONTransformer {
    return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
        @"single": @(CATRelationshipStatusSingle),
        @"relationship": @(CATRelationshipStatusInRelationship),
        @"complicated": @(CATRelationshipStatusComplicated)
    }];
}
BOOL ↔︎ JSON boolean
+ (NSValueTransformer *)awesomeJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
}

Create model objects from JSON

As soon as the model is fully configured, its time to get the JSON data from the API and convert it to an instance of our model. First, we need to convert the JSON representation to an NSDictionary, that can be used by Mantle to create our model. Luckily iOS provides a great way to do that via NSJSONSerialization.

After that, the MTLJSONAdapter class that ships with Mantle does the heavy-lifting to create our model.

// create NSDictionary from JSON data
NSData JSONData = ... // the JSON response from the API
NSDictionary *JSONDict = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:NULL];

// create model object from NSDictionary using MTLJSONSerialisation
CATProfile *profile = [MTLJSONAdapter modelOfClass:CATProfile.class fromJSONDictionary:JSONDict error:NULL];

Create JSON from model objects

MTLJSONAdapter is als capable of creating an NSDictionary from our model class that can be direcly encoded back into a JSON string.

// create NSDictionary from model class using MTLJSONSerialisation
CATProfile *profile = ...
NSDictionary *profileDict = [MTLJSONAdapter JSONDictionaryFromModel:profile];

// convert NSDictionary to JSON data
NSData *JSONData = [NSJSONSerialization dataWithJSONObject:profileDict options:0 error:NULL];

Note: If you have a property on your model that should not be included when creating a JSON represenatation of your model, return NSNull.null. E.g. @{"name": NSNull.null} in +JSONKeyPathsByPropertyKey. Mantle will savely ignore this property.

Mapping Arrays and Dictionaries

Most of the time, models have relationships to other models. These relationships are commonly represented via JSON arrays or objects (e.g. owner and friends).

{
  "id": 1,
  "name": "Objective Cat",
  ...,

  "owner": {
    "id": 99,
    "name": "Alexander Schuch"
  },
  "friends": [
    {
      "name": "Owly",
      "type": "bird"
    },
    {
      "name": "Hedgy",
      "type": "mammal"
    }
  ]
}

Mantle supports mapping these relationships to new models out of the box. In order to make sure Mantle knows how to transform the relationships we can use one of the following provided category methods on NSValueTransformer.

+ (NSValueTransformer *)mtl_JSONDictionaryTransformerWithModelClass:(Class)modelClass;
+ (NSValueTransformer *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass;

Of course Mantle needs to know about these relationships and their MTLModel subclasses they should be transformed to. Its as easy as creating new MTLModel subclasses and implementing the MTLJSONSerializing protocol for the objects that should be mapped. Then we can add some new properties to our CATProfile class and implement two new transformers.

// CATProfile.h
@property(strong, nonatomic) CATOwner *owner;   // CATOwner is a MTLModel subclass
@property(strong, nonatomic) NSArray *friends;  // Array of CATFriend objects

// CATProfile.m
+ (NSValueTransformer *)ownerJSONTransformer {
    return [NSValueTransformer mtl_JSONDictionaryTransformerWithModelClass:CATOwner.class];
}

+ (NSValueTransformer *)friendsJSONTransformer {
    return [NSValueTransformer mtl_JSONArrayTransformerWithModelClass:CATFriend.class];
}

Some nice additions

We briefly talked about NSValueTransformer before. NSValueTransformer has the nice feature that makes it possible to globally register a transformer by its name. In case you are using the same transformers all over your app, make sure to subclass NSValueTransformer, register your custom transformer once and subsequently use it in your MTLModels.

// In CATProfile.m
NSString * const kCATCustomValueTransformerName = @"CATCustomValueTransformer";

+ (void)initialize
{
  // Register NSValueTransformer
  if (self == CATProfile.class) {
    CATCustomValueTransformer *transformer = [CATCustomValueTransformer new];
    [NSValueTransformer setValueTransformer:transformer forName:kCATCustomValueTransformerName];
  }
}

// Then use the custom transformer to translate properties using Mantle
+ (NSValueTransformer *)whateverPropertyJSONTransformer {
    return [NSValueTransformer valueTransformerForName:kCATCustomValueTransformerName];
}

Conclusion

Mantle is a nice addition when working with JSON APIs. However, be aware that it might not be suitable for you if you have to deal with very complex and erratic APIs.

Try Mantle in your next project and let me know what you use it for on Twitter.

Further reading

Follow the Objective Cat

If you enjoyed this article, follow the Objective Cat and subscribe to the RSS feed.

© 2013-2014 — Alexander Schuch