Modeling JSON Mappings – Part 1

Modeling JSON Mappings – Part 1
Photo by Pankaj Patel / Unsplash

iOS apps commonly store and retrieve JSON data via REST APIs. Consequently, many development teams initially spend some time formulating an approach for decoding model objects from JSON, and (usually) vice versa. And due diligence requires sifting through a substantial number of frameworks, both in Objective-C and Swift, that provide varying degrees of support for mapping values between these two different representations. Unfortunately, even teams that adopt the best of these frameworks still tend to experience some headaches dealing with the resulting mappings.

Back to the Future

I've been a fan of object-relational mapping (ORM) frameworks since cutting my teeth on NeXT's Enterprise Objects Framework (EOF) in the mid-90s. ORMs are designed to deal with a host of issues that arise when mapping values between relational database tables and object models. Because JSON represents relationships more or less the same way model objects do --- hierarchically --- mapping JSON to model objects is inherently much simpler. Still, there are several capabilities ORMs and JSON mapping frameworks have in common:

Mandatory Capabilities

  • Map a given JSON dictionary to a specific class
  • Construct a model object on decode
  • Construct a dictionary on encode
  • Map JSON data values to object properties
  • Associate JSON element names with object property names
  • Allow specification of value transformations, and automatically apply them during encode/decode
  • Populate model object properties with JSON values on decode
  • Populate dictionary with model object property values on encode
  • Model to-one, and to-many relationships
  • Store type information for related objects
  • Construct child objects and arrays of child objects on decode
  • Construct child dictionaries and arrays of child dictionaries on encode

Nice to Haves

  • Flattened attributes
  • Inverse relationships

But the headaches developers always seem to run into using JSON mapping frameworks isn't because they lack these kinds of capabilities, but rather because the mappings have to be baked right into the code of each of the model classes. As a consequence, the data model is scattered across classes, making it harder to visualize, and harder to maintain.

But one of the killer features of EOF and its successor, Core Data, is its externalization of mapping metadata. Core Data mappings are coalesced into a single XML document that the framework works with at runtime. This has several advantages:

  • Core Data directly supports model versioning, allowing earlier versions of a given data model to be accessed at runtime, making it easier for apps to handle API version differences.
  • External tools can leverage the metadata to, for example, generate base classes (via Xcode's built-in class generation facilities, as well as third-party tools such as mogenerator)
  • An entire data model can be version controlled as a single unit, making differences between versions more apparent.
  • The model can be presented and edited in a visual tool such as Xcode's Model Editor

Give the potential advantages, the team here at About Objects wondered if it would be possible to a) use a Core Data model to store JSON mapping metadata (pretty much a no-brainer), and b) use Core Data models in projects that don'tuse Core Data for storage. Okay, we actually did more than 'wonder'; we wrote a framework, Modelmatic. Luckily, it turned out the answers were 'yes' and 'yes'.

The Modelmatic Framework

Modelmatic allows you to specify custom mappings between key-value pairs in JSON dictionaries, and corresponding properties of your model objects. For example, suppose you're working with JSON similar to the following (from the Modelmatic example app):

{
  "version" : "2",
  "batchSize" : "10",
  "authors" : [
    {
      "firstName" : "William",
      "lastName" : "Shakespeare",
      "born" : "1564-04-01",
      "author_id" : "101",
      "imageURL" : "https:\/\/www.foo.com?id=xyz123",
      "books" : [
        {
          "tags" : "drama,fantasy",
          "title" : "The Tempest",
          "year" : "2013",
          "book_id" : "3001"
        },
        ...

Step 1: Defining the Model

To use Modelmatic, you start by modeling your data using Xcode's Core Data Model Editor. Don't worry, you're not going to need to use other aspects of Core Data, just the data model -- and just a subset of it's capabilities.

Step 2: Create Swift Classes

If your model is complex, and/or changes frequently, consider using mogenerator to generate model classes (and update them as needed) from the metadata you specified in the model editor. Otherwise, it's simplest to just create the classes you need from scratch. Here's an example:

import Foundation
import Modelmatic

@objc (MDLAuthor)
 class Author: ModelObject
{
    // Name of the Core Data entity
    static let entityName = "Author"

    // Mapped to 'author_id' in the corresponding attribute's User Info dictionary
    var authorId: NSNumber!
    var firstName: String?
    var lastName: String?
    var dateOfBirth: NSDate?
    var imageURL: UIImage?

    // Modeled relationship to 'Book' entity
    var books: [Book]?
}

Key points:

  • import Modelmatic.
  • Subclass ModelObject.
  • Use @objc() to avoid potential namespacing issues.
  • Define a static let constant named entityName to specify the name of the associated entity in the Core Data model file.
  • authorId is mapped to author_id in the model (see the attribute definition's User Info dictionary).
  • Modelmatic automatically maps all the other properties, included the nested books property.

Customizing Mappings

Modelmatic automatically matches names of properties you specify as attributes or relationships in your Core Data model to corresponding keys in the JSON dictionary. For example, given an attribute named firstName, Modelmatic will try to use firstName as a key in the JSON dictionary, and map it to a firstName property in Author.

However, the framework also allows you to specify custom mappings as needed. For instance, the Author class has the following property:

var authorId: NSNumber!

A custom mapping is provided in the model file, binding the authorId attribute to the JSON key path author_id, as shown below:

To add a custom mapping, select an attribute or relationship in the model editor, and add an entry to it's User Infodictionary. The key should be jsonKeyPath, and the value should be the key or key path (dot-separated property path) used in the JSON dictionary. During encoding and decoding, Modelmatic will automatically map between your object's property, as defined by its attribute or relationship name, and the custom key path you specified to access JSON values.

Defining Relationships

Core Data allows you to define to-one and to-many relationships between entities. Modelmatic will automatically create and populate nested objects for which you've defined relationships. For instance, the Modelmatic example app defines a to-many relationship from the Author entity to the Book entity. To create an Author instance along with its nested array of books, you simply initialize an Author with a JSON dictionary as follows:

let author = Author(dictionary: $0, entity: entity)

For example, given the following JSON, the previous call would create and populate an instance of Author containing an array of two Book objects, with their author properties set to point back to the Author instance):

{
      "author_id" : "106"
      "firstName" : "Mark",
      "lastName" : "Twain",
      "books" : [
        {
          "book_id" : "3501",
          "title" : "A Connecticut Yankee in King Arthur's Court",
          "year" : "2014"
        },
        {
          "book_id" : "3502",
          "title" : "The Prince and the Pauper",
          "year" : "2015"
        }
      ],
    }

Property Types

Modelmatic uses methods defined in the NSKeyValueCoding (KVC) protocol to set model object property values. KVC can set properties of any Objective-C type, but has limited ability to deal with pure Swift types, particularly struct and enum types. However bridged Standard Library types, such as String, Array, Dictionary, as well as scalar types such as Int, Double, Bool, etc. are handled automatically by KVC with one notable issue: Swift scalars wrapped in Optionals. For example, KVC would be unable to set the following property:

var rating: Int?

If your ModelObject subclasses uses a Swift type that KVC can't directly handle, you can provide a computed property of the same name, prefixed with kvc_, to provide your own custom handling. For example, to make the rating property work with Modelmatic, add the following:

var kvc_rating: Int {
        get { return rating ?? 0 }
        set { rating = Optional(newValue) }
    }

If Modelmatic is unable to set a property directly (in this case the rating property), it will automatically call the kvc_prefixed variant (kvc_rating, in this example).

Specifying Value Transformations

In your Core Data model file, you can specify a property type as Transformable. If you do so, you can then provide the name of a custom transformer. For example, the Author class in the Modelmatic example app has a transformable property, dateOfBirth, of type NSDate. Modelmatic automatically uses an instance of the specified NSValueTransformersubclass to transform the value when accessing the property.

Here's the code of the Example app's DateTransformer class in its entirety:

import Foundation

@objc (MDLDateTransformer)
class DateTransformer: NSValueTransformer
{
    static let transformerName = "Date"

    override class func transformedValueClass() -> AnyClass { return NSString.self }
    override class func allowsReverseTransformation() -> Bool { return true }

    override func transformedValue(value: AnyObject?) -> AnyObject? {
        guard let date = value as? NSDate else { return nil }
        return serializedDateFormatter.stringFromDate(date)
    }

    override func reverseTransformedValue(value: AnyObject?) -> AnyObject? {
        guard let stringVal = value as? String else { return nil }
        return serializedDateFormatter.dateFromString(stringVal)
    }
}

private let serializedDateFormatter: NSDateFormatter = {
    let formatter = NSDateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    return formatter
}()

The date transformer is registered by the following line of code in the Example app's AuthorObjectStore class:

NSValueTransformer.setValueTransformer(DateTransformer(), forName: String(DateTransformer.transformerName))

Step 3: Loading the Model

Somewhere in your app (you only need to do this once during the app's lifecycle), do something like the following to load the Core Data model file into memory:

let modelName = "Authors"

guard let modelURL = NSBundle(forClass: self.dynamicType).URLForResource(modelName, withExtension: "momd"),
    model = NSManagedObjectModel(contentsOfURL: modelURL) else {
        print("Unable to load model \(modelName)")
        return
}

You'll most likely want to store the reference to the model in a class property.

Step 4: Encoding and Decoding Model Objects

Once you've obtained JSON data, you can deserialize it as follows (Note that deserializeJson wraps a call to NSJSONSerialization):

guard let data = data, dict = try? data.deserializeJson() else { 
    return
}

To construct an instance of your model class, simply provide the dictionary of deserialized values, along with the entity description:

let authors = Author(dictionary: $0, entity: entity)

This will construct and populate an instance of Author, as well as any nested objects for which you defined relationships in the model (and for which the JSON contains data). You then simply work with your model objects. Whenever you want to serialize an object or group of objects, simply do as follows:

// Encode the author
let authorDict = author.dictionaryRepresentation

// Serialize data
if let data = try? dict.serializeAsJson(pretty: true) {
    // Do something with the data...
}

Modelmatic provides methods to make it easier to programmatically set objects for properties that model to-one or to-many relationships. While it's easy enough to remove objects (simply set to-one properties to nil, or use array methods to remove objects from arrays), setting or adding objects to these properties can be slightly more involved. That's because Modelmatic automatically sets property values for any inverse relationships you define in your model, so that child objects will have references to their parents.

While inverse relationships aren't required, they're often convenient. Just be sure to use the weak lifetime qualifier for references to parent objects.

Even if you're not currently using inverse relationships, it's a good idea to use the convenience methods provided by ModelObject for modifying relationship values. That way, if you change your mind later, you won't need to change your code to add support for setting parent references.

To-Many Relationships

ModelObject provides two methods for modifying to-many relationships, as shown in the following examples:

// Adding an object to a to-many relationship
let author = Author(dictionary: authorDict, entity: authorEntity)
let book = Book(dictionary: bookDict, entity: bookEntity)
do {
    // Adds a book to the author's 'books' array, and sets the book's 'author' property
    try author.add(modelObject: book, forKey: "books")
}
catch MappingError.unknownRelationship(let name) {
    print("Unknown relationship \(name)")
}

// Adding an array of objects to a to-many relationship
let books = [Book(dictionary: bookDict2, entity: bookEntity),
             Book(dictionary: bookDict3, entity: bookEntity)]
do {
    // Adds two books to the author's 'books' array, setting each book's 'author' property
    try author.add(modelObject: books, forKey: "books")
}
catch MappingError.unknownRelationship(let name) {
    print("Unknown relationship \(name)")
}

To-One Relationships

An additional method is provided for setting the value of a to-one relationship, as shown here:

// Set the value of a to-one relationship
let book = Book(dictionary: bookDict1, entity: bookEntity)
let pricing = Pricing(dictionary: ["retailPrice": expectedPrice], entity: pricingEntity)
do {
    // Sets the book's 'pricing' property, and sets the pricing's 'book' property
    try book.set(modelObject: pricing, forKey: "pricing")
}
catch MappingError.unknownRelationship(let name) {
    print("Unknown relationship \(name)")
}

Next Installment

In Modeling JSON Mappings -- Part 2, we'll take a look under the hood to see how the Modelmatic framework leverages the data model to automate encoding and decoding.