Loading JSON with embedded records into Ember Data 1.0.0 beta

This is a follow-up to my last post that showed how to export an ember data object with embedded relationships into one nested JSON structure. This time we’ll take the same JSON and load it back into ember.

Here’s the JSON object we’ll be importing:

{"child": {
    "name": "Herbert",
    "toys": [{
        "kind": "Robot",
        "size": {
          "height": 5,
          "width": 5,
          "depth": 10
        },
        "features": [{
            "name": "Beeps"
          },{
            "name": "Remote Control"
        }]
    }]
  }
}

This should create a child record, with one toy that has size and a couple features. The model definition looks like this:

App.Child = DS.Model.extend({
    name: DS.attr('string'),
    toys: DS.hasMany('toy', {embedded: 'always'}),
});
Ember.Inflector.inflector.irregular("child", "children");
App.Toy = DS.Model.extend({
    kind: DS.attr('string'),
    size: DS.belongsTo('size', {embedded: 'always'}),
    features: DS.hasMany('feature', {embedded: 'always'})
});
App.Size = DS.Model.extend({
    height: DS.attr('number'),
    width: DS.attr('number'),
    depth: DS.attr('number')
});
App.Feature = DS.Model.extend({
    name: DS.attr('string')
});

Ember, however, needs all the relationships to be sideloaded, like this:

{
  "child": {
    "id": "child-1",
    "name": "Herbert",
    "toys": [
      "toy-1"
    ]
  },
  "toys": [
    {
      "id": "toy-1",
      "kind": "Robot",
      "size": "size-1",
      "features": [
        "feature-1",
        "feature-2"
      ]
    }
  ],
  "sizes": [
    {
      "id": "size-1",
      "height": 5,
      "width": 5,
      "depth": 10
    }
  ],
  "features": [
    {
      "id": "feature-1",
      "name": "Beeps"
    },
    {
      "id": "feature-2",
      "name": "Remote Control"
    }
  ]
}

If your server does not sideload relationships we can write a general serializer that reads the relationship information from your model definitions and automatically sideloads everything for you.

Why does Ember want you to sideload?

Before we go down the rabbit hole of importing embedded relationships, we should understand why sideloading should be considered the preferred approach, especially for data coming form a SQL database.

Imagine you’re building a blog and each post can have several tags. Some of these tags can be the same across multiple posts. Instead of duplicating tags in each post record, we can share these as individual records. Each post would then link to them by ID. If a particular tag is tracked or changed, it will share that tracking or changes with each post.

There are many other examples, but basically, sideloading is similar to normalizing a database. In a SQL database you don’t have one table for all information about a user/post/etc, you break them up into separate table with relationships. Similarly, ember doesn’t want you do have one giant nested JSON, it wants you to break the relationships out so they can be shared and reused. Although, I feel that Ember might be a bit too militant about this.

Converting the Payload

The code we’re about to write walks through the JSON payload from the server and automatically breaks the nested records into sideloaded arrays that Ember likes so much.

To start, we need to override the normalizePayload function in your ApplicationSerializer:

App.ApplicationSerializer = DS.RESTSerializer.extend({
    normalizePayload: function(type, payload) {
        this.extractRelationships(payload, payload[type.typeKey], type);
        return payload
    }
}

This calls a new function we’re going to create in ApplicationSerializer called extractRelationships. We’re going to start by writing it for belongsTo relationships.

_generatedIds: 0,

extractRelationships: function(payload, recordJSON, recordType){

    // Loop through each relationship in this record
    recordType.eachRelationship(function(key, relationship) {
        var related = recordJSON[key],                  // The record at this relationship
            type = relationship.type,                   // The model type
            sideloadKey = type.typeKey.pluralize(),     // The key for the sideload array 
            sideloadArr = payload[sideloadKey] || [],   // The sideload array for this item
            primaryKey = Ember.get(this, 'primaryKey'), // the key to this record's ID
            id = item[primaryKey];
        
        if (related && relationship.kind == 'belongsTo'){
            // Missing an ID, generate one
            if (typeof id == 'undefined') {
                id = 'generated-'+ (++this._generatedIds);
                item[primaryKey] = id;
            }

            // Don't add to sideload array if already exists there
            if (sideloadArr.findBy(primaryKey, id) != undefined){
                return payload;
            }

            // Add to sideloaded array
            sideloadArr.push(item);
            payload[sideloadKey] = sideloadArr;  

            // Replace object with ID
            recordJSON[key] = related.id;
            
            // Find relationships in this record
            this.extractRelationships(payload, related, type);
        }
    }, this);
    
    return payload;
}

This would take a JSON like this:

{
  "toys": [{
      "kind": "Robot",
      "size": {
        "height": 5,
        "width": 5,
        "depth": 10
      }
  }]
}

and turn it into this:

{
  "toys": [
    {
      "kind": "Robot",
      "size": "size-1"
    }
  ],
  "sizes": [
    {
      "id": "size-1",
      "height": 5,
      "width": 5,
      "depth": 10
    }
  ]
}

That’s good, but only handles one-to-one relationships. To do the same thing with hasMany relationshipos, you’ll need to loop through each of the objects in that hasMany array. Now we’ll pull the bulk of the sideloading logic out of extractRelationships into it’s own method, sideloadItem, that can be used for both relationships.

Here’s what the code looks like now:

App.ApplicationSerializer = DS.RESTSerializer.extend({
    /**
     The current ID index of generated IDs
     @property
     @private
    */
    _generatedIds: 0,
    
    /**
     Sideload a JSON object to the payload
     
     @method sideloadItem
     @param {Object} payload   JSON object representing the payload
     @param {subclass of DS.Model} type   The DS.Model class of the item to be sideloaded
     @param {Object} item JSON object   representing the record to sideload to the payload
    */
    sideloadItem: function(payload, type, item){
        var sideloadKey = type.typeKey.pluralize(),     // The key for the sideload array 
            sideloadArr = payload[sideloadKey] || [],   // The sideload array for this item
            primaryKey = Ember.get(this, 'primaryKey'), // the key to this record's ID
            id = item[primaryKey];
                    
        // Missing an ID, generate one
        if (typeof id == 'undefined') {
            id = 'generated-'+ (++this._generatedIds);
            item[primaryKey] = id;
        }
        
        // Don't add if already side loaded
        if (sideloadArr.findBy("id", id) != undefined){
            return payload;
        }
        
        // Add to sideloaded array
        sideloadArr.push(item);
        payload[sideloadKey] = sideloadArr;  
        return payload;
    },
    
    /**
     Extract relationships from the payload and sideload them. This function recursively 
     walks down the JSON tree
     
     @method sideloadItem
     @param {Object} payload   JSON object representing the payload
     @paraam {Object} recordJSON   JSON object representing the current record in the payload to look for relationships
     @param {Object} recordType   The DS.Model class of the record object
    */
    extractRelationships: function(payload, recordJSON, recordType){
        // Loop through each relationship in this record type
        recordType.eachRelationship(function(key, relationship) {
            var related = recordJSON[key], // The record at this relationship
                type = relationship.type;  // The model type
            
            if (related){
                
                // One-to-one
                if (relationship.kind == "belongsTo") {
                    // Sideload the object to the payload
                    this.sideloadItem(payload, type, related);
    
                    // Replace object with ID
                    recordJSON[key] = related.id;
                    
                    // Find relationships in this record
                    this.extractRelationships(payload, related, type);
                }
                
                // Many
                else if (relationship.kind == "hasMany") {
    
                    // Loop through each object
                    related.forEach(function(item, index){
    
                        // Sideload the object to the payload
                        this.sideloadItem(payload, type, item);
    
                        // Replace object with ID
                        related[index] = item.id;
                        
                        // Find relationships in this record
                        this.extractRelationships(payload, item, type);
                    }, this);
                }
                
            }
        }, this);
        
        return payload;
    },


    /**
     Overrided method
    */
    normalizePayload: function(type, payload) {
        var typeKey = type.typeKey,
            typeKeyPlural = typeKey.pluralize();
        
        payload = this._super(type, payload);
        
        // Many items (findMany, findAll)
        if (typeof payload[typeKeyPlural] != "undefined"){
            payload[typeKeyPlural].forEach(function(item, index){
                this.extractRelationships(payload, item, type);
            }, this);
        }
        
        // Single item (find)
        else if (typeof payload[typeKey] != "undefined"){
            this.extractRelationships(payload, payload[typeKey], type);
        }
        
        return payload;
    },
});

Drawbacks

The biggest drawback that I have found with this is when your JSON does not have IDs. In order for Ember to keep track of records, everything must have and ID; so this code auto generates them. IDs are used by Ember to know which items are unique. If your JSON doesn’t have any, then all records — no matter if they’re the same — will be given a generated ID and seen as unique and then a findAll will return them all. (You can see this bug in the live demo by removing the IDs from the sample data)

In short, it’s best to use IDs at least on records that might be loaded multiple times in a page session.

Live Demo

I put this example up on JSFiddle so you can see and play with it: http://jsfiddle.net/jgillick/TPhtC/

Remove the IDs from the sample data and you’ll see it return 3 records for the global find call.