Extend DS.ActiveModelSerializer support for embedded objects; `belongsTo` relationship using `has_one`


#1

Problem

An embedded object in a JSON payload, that is a result of using has_one :XXXX, embed: :objects with an instance of ActiveModel:Serializers, is not supported yet in the DS.EmbeddedRecordsMixin.

Using Mongoid w/ Rails the ActiveModel::Serializer allows embedded objects via has_many and has_one relationships. Currently Ember Data’s ActiveModelSerializer has support for embedded hasMany relationships using methods: extractSingle, extractArray and serialize.

However, a payload with a format that includes embedded objects using a belongsTo relationship with a payload created using has_one is not supported by the DS.ActiveModelSerializer.

For example: If a server stores a document (e.g. mongodb) with an embedded object, using has_one in the serializer, the same embedded payload should be used to create, read and update the document (POST/GET/PUT).

Proposed Solution

See the ActiveModelSerializer test, for the various objects used to test the serializer. Adding a new SecretLab that belongsTo a SuperVillain (likewise the SuperVillain will belongTo the SecretLab, effecively a 1:1 relationship)

SecretLab = DS.Model.extend({
  minionCapacity: DS.attr('number'),
  vicinity:       DS.attr('string'),
  superVillain:   DS.belongsTo('superVillain')
});
SuperVillain = DS.Model.extend({
  firstName:     DS.attr('string'),
  lastName:      DS.attr('string'),
  homePlanet:    DS.belongsTo("homePlanet"),
  secretLab:     DS.belongsTo("secretLab"),
  evilMinions:   DS.hasMany("evilMinion")
});

Given a choice made on the server to embed this 1:1 relationship, the original payload from server (w/ embedded belongsTo object for secretLab) would look like:

{
  "super_villain": {
    "id": "1",
    "first_name": "Tom",
    "last_name": "Dale",
    "home_planet_id": "123",
    "evil_minion_ids": ["1", "2", "3"],
    "secret_lab": {
      "id": "101",
      "minion_capacity": 5000,
      "vicinity": "California, USA"
    }
  }
}

And for Ember data to parse the payload the format needs to be converted to the compound document format (side loaded). So using the extractSingle method the embedded belongsTo object would be extracted to the needed format like so:

{    
  "superVillain": {
    "id": "1",
    "firstName": "Tom",
    "lastName": "Dale",
    "homePlanet": "123",
    "evilMinions": ["1", "2", "3"],
    "secretLab": "101"
  },
  "secretLabs": [
    {
      "id": "101",
      "minionCapacity": 5000,
      "vicinity": "California, USA"
    }
  ]
}

Likewise when serializing… setting a custom attrs configuration will be used to indicate that the model is always embedded:

App.SuperVillainSerializer = DS.ActiveModelSerializer.extend({
  attrs: {
    secretLab: {embedded: 'always'}
  }
}));

Given a record created like so:

// record with id(s), persisted
var tom = this.store.createRecord(
  'superVillain',
  { firstName: "Tom", lastName: "Dale", id: "1",
    secretLab: this.store.createRecord('secretLab', { minionCapacity: 5000, vicinity: "California, USA",   id: "101" }),
    homePlanet: this.store.createRecord('homePlanet', { name: "Villain League", id: "123" })
  }
);

A test to confirm that the SuperVillain is serialized with an embedded object would look like:

deepEqual(tom.serialize(), {
  first_name: tom.get("firstName"),
  last_name: tom.get("lastName"),
  home_planet_id: tom.get("homePlanet").get("id"),
  secret_lab: {
    id: tom.get("secretLab").get("id"),
    minion_capacity: tom.get("secretLab").get("minionCapacity"),
    vicinity: tom.get("secretLab").get("vicinity")
  }
});

And for a new record (no id for the villian or lab yet, planet is known):

var tom = this.store.createRecord(
  'superVillain',
  { firstName: "Tom", lastName: "Dale",
    secretLab: this.store.createRecord('secretLab', { minionCapacity: 5000, vicinity: "California, USA" }),
    homePlanet: this.store.createRecord('homePlanet', { name: "Villain League", id: "123" })
  }
);

A test to confirm the serialized record without any ids would be like:

deepEqual(tom.serialize(), {
  first_name: tom.get("firstName"),
  last_name: tom.get("lastName"),
  home_planet_id: tom.get("homePlanet").get("id"),
  secret_lab: {
    minion_capacity: tom.get("secretLab").get("minionCapacity"),
    vicinity: tom.get("secretLab").get("vicinity")
  }
});

Discussion

I have already written a custom application serializer for a project I’m working on (which extends DS.ActiveModelSerializer). I’ve started to write failing tests for the above proposed solution in the ActiveModelSerializer test file. Personally I would like to extend the DS.ActiveModelSerializer to support embedded hasMany and belongsTo payloads using both embeds_many and embeds_one in model classes.

Is anyone interested in the proposed solution above?


#3

If anyone is interested… I have a working adapter/serializer, activemodelmongoid-adapter, (for using Rails/Mongoid/ActiveModelSerializers) shared in a new repository: ember-data-extensions


#4

Can I use this with regular RestSerializer instead of ActiveModelSerializer?


#5

Yep, that is how I am using it, see the embedded-adapter initializer.js as an example.


#6

Cool. I’m reading the doc & it states:

Embedding objects/arrays 1 level deep is supported.

Some of my models are unfortunately heavily nested. If I can’t achieve this with your mixin, do you know if it’s possible to do this by explicitly implementing serializers per model? It would be great if you could give me any direction. Thanks.


#7

@beku8, yeah if you do have embedded objects within embedded objects you may need a serializer for each type to define the attrs that are embedded. I haven’t tried that. I have used the mixins w/ one level of embedded objects so that was what I was comfortable to support. However, looking at the tests for the embedded mixin there are tests for embedded within embedded records:

The EmbeddedMixin mixin is a fork of the DS.EmbeddedRecordsMixin which provided the above functionality.


#8

@beku8, @emk mentioned that “DS.EmbeddedRecordsMixin, it actually handles multiply-embedded records quite well” see: https://github.com/emberjs/data/pull/1633#issuecomment-31795647


#9

I’ve added some config options for serializing ids vs records as well as a method to setup how to handle foreign keys, see: https://gist.github.com/igorT/ec4d5eadefe08fa90274 and the updated PR https://github.com/emberjs/data/pull/1637 see serializePropertyForRelationship