Mainframe by foast

Parsing JSON is like plumbing - it must exist and work flawlessly, and nobody likes implementing it. iOS has built-in JSON-parsing abilities; however, as this Big Nerd Ranch blog post explains, it is full of gotchas. For example, if you add a field to the payload that's not a part of your model, your app will crash. The built-in SDK also does not provide an architecture to map field names, or to inject custom code that converts JSON string values to other objects, such as dates. Things get even worse when your JSON represents a hierarchy of objects (nested objects).

You can avoid all of these gotchas by using Mantle, which:

  • lets you map property name differences, such as "created_on_mk2521_weird_name" field in your JSON to "dateCreated" property on your model
  • does not blow up when JSON has extra fields compared to the model
  • allows you to inject Transformers - objects tasked with converting string values to objects and vice versa (if you need to serialize back to JSON)
  • lets you ignore some properties on your model when it's time to serialize back to JSON, if you want
  • is awesome

The Setup

But a picture is worth a thousand words. We just released an open-source project to allow default app selection in iOS called Choosy. One of our model classes in it looks like this:

Here, I want just the top 4 properties to be mapped from and to JSON. Then I want the app URL, which is a string in JSON, to be converted to an NSURL object in the process. I also want the appActions array to be populated with instances of ChoosyAppAction objects using the data in the same JSON. Finally, I want Mantle to ignore the isInstalled property when reading or writing JSON.

The JSON for this model looks like this:

Doing the Work

To deserialize this JSON as ChoosyAppInfo objects with Mantle I need to do three things.

1: Inherit from MTLModel

2: Map Fields

In the code below, I tell Mantle which property names on the model correspond to which field names in JSON, how to convert a string into an NSURL for appURLScheme property, and what type of objects are in the appActions array:

The Transformer methods must be named using the combination of property name followed by "JSONTransformer", such as appActionsJSONTransformer:. This naming convention is not optional.

3: Map Nested Objects

Your model will likely contain references to other models (nested objects). In our case, for Mantle to populate the appActions property with ChoosyAppAction objects, the ChoosyAppAction class also needs to inherit from MTLModel and provide own property name mappings, like so:

Enjoying the Results (Deserialization)

Now that ChoosyAppInfo contains the mappings, I can deserialize a dictionary full of JSON data into an array of ChoosyAppInfo objects using MTLJSONAdapter's modelsOfClass:fromJSONArray:error: method:

(If your JSON starts with a dictionary, use the modelOfClass:fromJSONDictionary:error: method instead.)

Finally, if you're not using a 3rd-party networking SDK like AFNetworking that can return JSON data directly, you will need to parse the NSData objects you get from iOS's networking SDK into JSON. Luckily, iOS's NSJSONSerialization class does just that:

Serialization

To serialize into JSON, you simply call MTLJSONAdapter's JSONArrayFromModels: method (or JSONDictionaryFromModel: if you have just one object):

The resulting NSData object can be written to a file or sent via a network.

That's really it. Outside of mapping, data type transformations, and nested objects declarations, Mantle boils down to just a few methods:

MTLJSONAdapter modelsOfClass:fromJSONArray:error: & MTLJSONAdapter modelOfClass:fromJSONDictionary:error: to get objects from JSON,

and

MTLJSONAdapter JSONArrayFromModels & MTLJSONAdapter JSONDictionaryFromModel: to turn objects into JSON.

Tips and Gotchas

Required Method

No matter what, you always need to implement +(NSDictionary *)JSONKeyPathsByPropertyKey method in all of your model classes that inherit from MTLModel. If there's nothing to map (model property names match JSON field names), then return an empty dictionary, like so:

Otherwise, Mantle will throw a runtime exception. You can work around that, if you like, by providing a global parent class for all your model objects to inherit from, and implementing the method in that class:

And having your model classes inherit from MTLModelModified instead of MTLModel, overriding the method if needed.

I did submit a pull request that fixes this; the code would just assume an empty dictionary of name mappings if +(NSDictionary *)JSONKeyPathsByPropertyKey wasn't implemented. The pull request was rejected because the team decided to require a full, explicit model-to-JSON map for each object in the upcoming major release of Mantle. So in v2.0 you'll need to map every property name explicitly to its corresponding JSON field name.

Thankfully, the team is introducing a method that creates the map for you, so you can still employ the global-parent-class strategy above and just change its implementation to call this new method:

Data Type Conversion

Above I showed how to use Mantle's built-in string-to-NSURL transformer. But what if you want to write custom string-to-object conversion code? Here's how, using the example of a string-to-NSDate. Let's say you have this epic model:

In the implementation file of this model, you need to create a class method whose name starts with your property's name and ends with "JSONTransformer":

I kept it simple by relying on NSDateFormatter's built-in methods (dateFromString: and stringFromDate:), but you can roll your own, custom logic there instead.

The full code listing for the model's implementation file:

Note that it's up to you to figure out how to handle instantiation of NSDateFormatter (since instantiating one is a relatively expensive operation). My code shows the simplest solution (a static instance on the model itself), but you may want have a dedicated singleton that serves out an instance of NSDateFormatter and contains code to nil out the instance if memory runs low, etc. That way you can use the same NSDateFormatter instance from multiple places in your app.

Core Data?

Using Core Data means that your model classes inherit from NSManagedObject, which means that they can't inherit from MTLModel. In this situation, you can still use Mantle if you want, but you will need to define a separate model hierarchy for any data represented as JSON. I don't want to dig deep into the topic of concerns separation, but the truth is that any data coming down the pipe is not your real domain model anyway. It's often metadata, or a shortened version of the domain model, or just IDs that point to the actual domain model objects, etc.

So the fact that we can even inherit our domain model classes from MTLModel is, in a way, a hack - a shortcut, if you will, for the simpler cases. In a bigger application, web service communication will often need to be proxied by a tier that can translate between the domain model of your client app and that of your server.

See also 'Why Not Use Core Data?' in Mantle's Readme.

Conclusion

Mantle is one of the easiest and most flexible JSON deserialization frameworks for iOS. It's fast, it's flexible, and it allows for a neat separation of concerns. It appears to be a far superior and more complete solution to using the built-in SDK methods, and this post covered all there is to get started. We covered a lot here, so don't let the size of the post scare you from Mantle - it's really easy to implement. You can see how we did it in Choosy by looking at the files in the Choosy/Model folder.

Happy Mantling!

Main image from Flickr user foast.