Serializing Embedded Relationships with Ember Data 1.0.0 beta

In the latest Ember Data (currently 1.0.0-beta.4) saving or serializing embedded records doesn’t work the way some of us wish. By default it will serialize the parent record and include a list of IDs for the nested records. This is good practice and fine for data that will be stored in a SQL database, however, my project needs to export the entire structure into a single JSON that will be saved to a file. Even though you cannot do this out of the box, adding just a few lines of code will get it working!

Desired Behavior

For this example we’ll create a child that can have many toys. This would be a simple hasMany relationship:

App.Child = DS.Model.extend({
    name: DS.attr('string'),
    toys: DS.hasMany('toy'),
});
App.Toy = DS.Model.extend({
    kind: DS.attr('string')
});

If Max has a Kazoo, the serialized JSON should look like this:

child: {
  name: "Max",
  toys: [{
      name: "Kazoo"
  }]
}

However, by default Ember will simply list the Kazoo ID in the toy’s list. Which, I will state again, is excellent practice for a traditional application but doesn’t work for the case where we want everything encapsulated in a single JSON.

Overriding Your Serializer

All you need to do is override the Serializer that handles hasMany relationships. You could have it export all relationships automatically, but this can cause some bad problems with cyclical relationships; instead make it optional with the familiar embedded:'always' flag. Start by updating your model with the embedded flag:

App.Child = DS.Model.extend({
    name: DS.attr('string'),
    toys: DS.hasMany('toy', {embedded: 'always'}),
});

Now write your serializer:

DS.JSONSerializer.reopen({ // or DS.RESTSerializer
    serializeHasMany: function(record, json, relationship) {
        var key = relationship.key,
            hasManyRecords = Ember.get(record, key);
        
        // Embed hasMany relationship if records exist
        if (hasManyRecords && relationship.options.embedded == 'always') {
            json[key] = [];
            hasManyRecords.forEach(function(item, index){
                json[key].push(item.serialize());
            });
        }
        // Fallback to default serialization behavior
        else {
            return this._super(record, json, relationship);
        }
    }
});

Now when you serialize a child object, all the nested toy objects will be included.

Add belongsTo Relationships

This handles the hasMany relationships, but we’ll probably also want to support belongsTo. For example, what if each toy had dimensions:

App.Toy = DS.Model.extend({
    kind: DS.attr('string'),
    size: DS.belongsTo('size', {embedded: 'always'})
});
App.Size = DS.Model.extend({
    height: DS.attr('number'),
    width: DS.attr('number'),
    depth: DS.attr('number')
});

To support this, we do the same thing as before but with the serializeBelongsTo method:

serializeBelongsTo: function(record, json, relationship) {
    var key = relationship.key,
        belongsToRecord = Ember.get(record, key);
    
    if (relationship.options.embedded === 'always') {
        json[key] = belongsToRecord.serialize();
    }
    else {
        return this._super(record, json, relationship);
    }
}

Now when you serialize a child object, the resulting JSON will look something like this:

child: {
  name: "Herbert",
  toys:[{
    kind: "Kazoo",
    size: {
      height: 5,
      width: 5,
      depth: 10
    }
  }]
}

Demo

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

Beware of Cyclical Relationships!

One problem you might run into is when two objects point at eachother. Take this example from the Ember site:

App.Post = DS.Model.extend({
  comments: DS.hasMany('comment')
});
App.Comment = DS.Model.extend({
  post: DS.belongsTo('post')
});

If you marked both of these relationships as embedded:'always', it would create an infinite loop of serialization. A post would include comments which would include the post and so on.

In this case, it’s best to decide which direction you want your object to nest and only put the embedded option on one of them. For example:

App.Post = DS.Model.extend({
  comments: DS.hasMany('comment', {embedded: 'always'})
});
App.Comment = DS.Model.extend({
  post: DS.belongsTo('post')
});