Saving Models and Their Relationships with JSON API

Loading models with included relationships is straightforward with Ember Data. But saving a model along with its relationships at once (in one payload)? Not so much.

This is a common scenario – whether to simplify code, reduce roundtrips to the server or make the payload transactional.

But it seems to have no intuitive solution. The current JSON API spec does not support it. Trying to solve this can make us waste time and want to give up.

Let’s explore this problem using an Invoice has many Lines example. And find a workaround!

Status quo: Saving relationships sequentially

For example:  

  1. save children to the server
  2. get children back with ids assigned
  3. set the relationship
  4. save the parent
// app/routes/application.js

export default Ember.Route.extend({

  actions: {

    submit() {

      const invoice = this.store.createRecord('invoice', { client: "Ricky Fort" });

      const lines = [
        this.store.createRecord('line', { description: "Chocolate", price: 2.10 }),
        this.store.createRecord('line', { description: "Red Bull", price: 1.70 }),
        this.store.createRecord('line', { description: "Ball pen", price: 4.25 })
      ];

      Ember.RSVP.all(lines.map(line => line.save()).then((lines) => {
        invoice.set('lines', lines);
        invoice.save();
      });

    }

  }

});

There is really no way of saving just the invoice and lines along with it?

// app/routes/application.js

export default Ember.Route.extend({

  actions: {

    submit() {

      const invoice = this.store.createRecord('invoice', { client: "Ricky Fort" });
      this.store.createRecord('line', { description: "Chocolate", price: 2.10, invoice });
      this.store.createRecord('line', { description: "Red Bull", price: 1.70, invoice });
      this.store.createRecord('line', { description: "Ball pen", price: 4.25, invoice });
      invoice.save();

    }

  }

});

Well… we can tell the Invoice serializer to serialize Lines…

// app/serializers/invoice.js

import JSONAPISerializer from 'ember-data/serializers/json-api';

export default JSONAPISerializer.extend({

  attrs: {
    lines: { serialize: true }
  }

});

Inspecting the payload upon save gets us this promising payload: included relationships!

{
  data: {
    type: "invoices",
    attributes: {
      client: "Ricky Fort"
    },
    relationships: {
      lines: {
        data: [
          { id: null, type: "lines"},
          { id: null, type: "lines"},
          { id: null, type: "lines"}
        ]
      }
    }
  }
}

Alas, not enough… where is the actual Lines data?

How do we send one request payload for all changes, all at once? Something like “deep saving” multiple models?

Ember Data’s DS.EmbeddedRecordsMixin enables us to do precisely that: post embedded model relationships… but it is not compatible with JSONAPISerializer!

So the real question becomes: is there a correct way to save a model and all its relationships included in a single post payload using JSON API serialization?

Saving a model with its hasMany relationship at once

Currently not. At least not officially. Being result-oriented developers, we will try to find a workaround that we can use today.

Imagine that we made our serializer also include attributes in our relationships. That saving a new Invoice with many new Lines produced this payload:

{
  data: {
    type: "invoices",
    attributes: {
      client: "Ricky Fort"
    },
    relationships: {
      lines: {
        data: [
          {
            id: null,
            type: "lines",
            attributes: { description: "Chocolate", price: 2.10 }
          },
          {
            id: null,
            type: "lines",
            attributes: { description: "Red Bull", price: 1.70 }
          },
          {
            id: null,
            type: "lines",
            attributes: { description: "Ball pen", price: 4.25 }
          }
        ]
      }
    }
  }
}

And that upon successful saving of all four models, our server JSON API returned the following:

{
  data: {
    id: 1,
    type: "invoices",
    attributes: {
      client: "Ricky Fort"
    },
    relationships: {
      lines: {
        data: [
          {
            id: 1,
            type: "lines",
            attributes: { description: "Chocolate", price: 2.10 }
          },
          {
            id: 2,
            type: "lines",
            attributes: { description: "Red Bull", price: 1.70 }
          },
          {
            id: 3,
            type: "lines",
            attributes: { description: "Ball pen", price: 4.25 }
          }
        ]
      }
    }
  }
}

Saved models with ids assigned. Wouldn’t that be awesome?!

I built an add-on:

$ ember install ember-data-save-relationships

Let’s put it to use!

// app/serializers/invoice.js

import JSONAPISerializer from 'ember-data/serializers/json-api';
import SaveRelationshipsMixin from 'ember-data-save-relationships';

export default JSONAPISerializer.extend(SaveRelationshipsMixin, {

  attrs: {
    lines: { serialize: true }
  }

});

And our relationships’ attributes are magically included in our payload!

Server-side handling

There is only one gotcha. In order to identify outgoing/incoming models, we automatically pass a temporary internal __id__ in our embedded record attributes. Like this:

{
  id: 1,
  type: "lines",
  attributes: {
    description: "Chocolate",
    price: 2.10,
    __id__: "1internal-model"
  }
}

Your backend API must receive and return this __id__ attribute intact.

Additionally, of course, your server is responsible for:

  • accessing relationship records and storing them appropriately
  • generating ids for every record
  • return the same structure – with updated id and attributes if applicable

Here is a super basic mock server if you need one for testing purposes. No guarantees.

// server/index.js

module.exports = function(app) {

  app.post('/invoices', function(req, res) {

    const id1 = "1internal-model";
    const id2 = "2internal-model";
    const id3 = "3internal-model";

    const json = {
      data: {
        id: 1,
        type: "invoices",
        attributes: {
          client: "Ricky Fort"
        },
        relationships: {
          lines: {
            data: [
              {
                id: 1,
                type: "line",
                attributes: { __id__: id1, description: "Chocolate", price: 2.10 }
              },
              {
                id: 2,
                type: "line",
                attributes: { __id__: id2, description: "Red Bull", price: 1.70 }
              },
              {
                id: 3,
                type: "line",
                attributes: { __id__: id3, description: "Ball pen", price: 4.25 }
              }
            ]
          }
        }
      }
    }
    res.send(json);
  });

  app.post('/lines', function(req, res) {

    const id1 = "0internal-model";

    const json = {
      data: {
        id: 1,
        type: "lines",
        attributes: {
          client: "Ricky Fort"
        },
        relationships: {
          invoices: {
            data: {
              id: 1,
              type: "invoice",
              attributes: { __id__: id1, client: "Ricky Fort" }
            }
          }
        }
      }
    }
    res.send(json);
  });

}

So now we can save() like this, and it works!

// app/routes/application.js

export default Ember.Route.extend({

  actions: {

    submit() {

      const invoice = this.store.createRecord('invoice', { client: "Ricky Fort" });
      this.store.createRecord('line', { description: "Chocolate", price: 2.10, invoice });
      this.store.createRecord('line', { description: "Red Bull", price: 1.70, invoice });
      this.store.createRecord('line', { description: "Ball pen", price: 4.25, invoice });
      invoice.save();

    }

  }

});

Check out the inspector:

Saving a belongsTo relationship

Alternatively, we could do:

// app/routes/application.js

export default Ember.Route.extend({

  actions: {

    submit() {

      const invoice = this.store.createRecord('invoice', { client: "Ricky Fort" });
      const line = this.store.createRecord('line', { description: "Chocolate", price: 2.10, invoice });
      line.save();

    }

  }

});

We must remember to activate serialization (and include the mixin) in the appropriate serializer:

// app/serializers/line.js

import JSONAPISerializer from 'ember-data/serializers/json-api';
import SaveRelationshipsMixin from 'ember-data-save-relationships';

export default JSONAPISerializer.extend(SaveRelationshipsMixin, {

  attrs: {
    invoice: { serialize: true }
  }

});

I for one would love to see how the final JSON API 1.1 specification winds up!

Does this help? What do you think? Looking forward to your feedback or any issues you may encounter!

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember:

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)