How to implement polymorphic models in Ember JS?

Please does anyone know how to implement polymorphic models with DS.RESTSerializer? So far I have created a model called ‘feed-item’ that has one attribute ‘feedables’ that describes a polymorphic belongs to relationship with one of two different types of models that extend feedable:

model/feed-item.js

import DS from 'ember-data';
export default DS.Model.extend({
  feedable: DS.belongsTo('feedable', { polymorphic: true })
})

The feedable model and models that extend it:

model/feedable.js

import DS from 'ember-data';
export default DS.Model.extend({
  feedItem: DS.belongsTo('feed-item')
})

model/video-item.js

import DS from 'ember-data';
import Feedable from './feedable';
export default Feedable.extend({
  content: DS.attr('string')
})

model/report-item.js

import DS from 'ember-data';
import Feedable from './feedable';
export default Feedable.extend({
  status: DS.attr('number')
})

To get the model for my feed route I do the following in the route:

route/feed.js

model(){
    return this.store.findAll('feed-item')
}

The response that the backend sends looks like so:

 {
    feed_items: [
      {id: 1, feedable: {id: 1, type: 'videoItem'}} ,
      {id: 2, feedable: {id: 2, type: 'reportItem'}} ,
      {id: 3, feedable: {id: 3, type: 'videoItem'}} ,
    ],
    video_items: [
       {id: 1, content: 'some video'},
       {id: 3, content: 'another video'}
    ],
    report_item:  {id: 2, status: 3},
 }

However this is not working for me. Am I missing something? Or is this setup completely wrong. I am using the DS.RESTSerializer in my ember app however I did not do anything special there for normalizing this response. Please any help will be appreciated.

Hi @amexaCree, I think your issue is that that response format doesn’t look like something I would expect the RESTSerializer to understand. Though I admit I am pretty rusty on what it does. If you control the response format, you’ll have an easier time if you switch to json:api, just because it’s a much more precise and well-documented format.

For example, if you deleted your RESTSerializer entirely and changed the response to:

{
  data: [
    { 
        type: "video-items",
        id: 1,
        attributes: {
           content: 'some video'
        }
    },{
        type: "report-items",
        id: 2,
        attributes: {
           status: 3
        }
    },{ 
        type: "video-items",
        id: 3,
        attributes: {
           content: 'another video'
        }
    }
  ]
}

It should Just Work. But assuming you need to keep the response format you showed above, we can write a serializer that transforms that into what I pasted above. Here is a solution that I tested to confirm it works.

My models (notice I have three, not four):

// models/feed-item.js
import DS from 'ember-data';
export default DS.Model.extend({});

// models/video-item.js
import DS from 'ember-data';
import FeedItem from './feed-item';
export default FeedItem.extend({
  content: DS.attr('string')
})

// models/report-item.js
import DS from 'ember-data';
import FeedItem from './feed-item';
export default FeedItem.extend({
  status: DS.attr('number')
})

My serializer:

import DS from 'ember-data';
import { underscore, dasherize } from '@ember/string';
import { pluralize } from 'ember-inflector';

export default DS.JSONAPISerializer.extend({

  // this is complex because your response format is complex. It
  // would be better to have a simpler format. For example, allowing
  // both singular and plural section names adds a lot of code here.
  normalizeResponse(store, primaryModelClass, payload, id, requestType) {
    if (requestType === 'findAll') {
      return {
        data: payload.feed_items.map(feedItem => {
          let sectionKey = underscore(feedItem.feedable.type);
          if (payload[sectionKey]) {
            return convertItem(feedItem, payload[sectionKey]);
          }
          if (payload[pluralize(sectionKey)]) {
            return convertItem(feedItem, payload[pluralize(sectionKey)].find(i => i.id === feedItem.id));
          }
        })
      }
    }
    return this._super(store, primaryModelClass, payload, id, requestType);
  }
});

function convertItem(feedItem, attrs) {
  let attributes = Object.assign({}, attrs);
  delete attributes.id;
  return {
    type: dasherize(feedItem.feedable.type),
    id: attrs.id,
    attributes
  }
}

After that, store.findAll('feed-item') properly loads the three models in your sample response above.

Notice I don’t have any belongsTo. That’s not needed just to get polymorphism. It’s enough that we use inheritance.

1 Like

I should add: the docs for normalizeResponse are here. It receives whatever your server sent as its payload argument, and it should return the payload modified to be standard json:api.