(Updated 2016-04-03: PR merged)
Two weeks ago I gave a talk at EmberJS BCN Meetup on "How to use Ember Data with your REST API". It was my first talk and fortunately it didn't go bad. I wanted to explain how can you customize Ember Data to work with APIs that don't comply with JSON API spec, which is the default Response format expected by Ember Data
since version 2.0.
However, while I was livecoding a custom DS.RESTSerializer
as a demo, I realized that normalizeHash
property wasn't listed on the API docs anymore (Ember Data 2.4 docs), it was referenced within the normalize
method definition, though.
Last week I started digging into this topic and realized that it was deprecated on Ember Data 1.13 release. However, docs were only partially updated and normalizeHash
didn't appear anymore as property of DS.RESTSerializer
, but the normalize
method still had references:
If you want to do normalizations specific to some part of the payload, you can specify those under
normalizeHash
.For example, if the
IDs
under"comments"
are provided as_id
instead ofid
, you can specify how to normalize just the comments:
//app/serializers/post.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizeHash: {
comments: function(hash) {
hash.id = hash._id;
delete hash._id;
return hash;
}
}
});
Then, I found an issue in Ember Data's repo asking to solve this. I opened a PR that fixes it (already merged). Also, a deprecation warning has been added by PR 4258.
Munging the payload
So, what's the right way to use RESTSerializer?
If you need to do general manipulations to your API's response, start with the normalizeResponse
method. Let's say that our non-JSON API Football Teams API has the following endpoint/response:
GET /api/teams
{
"result": "Ok",
"data": {
"teams": [
{
"name": "Leicester City F.C.",
"league": "Premier League",
"points": 63,
"won": 18,
"drawn": 9,
"lost": 9,
"gf": 53,
"ga": 31,
"founded": 1884,
"arena": "King Power Stadium"
},
{
"name": "Totteham Hotspur F.C.",
"league": "Premier League",
"points": 58,
"won": 16,
"drawn": 10,
"lost": 4,
"gf": 53,
"ga": 24,
"founded": 1882,
"arena": "White Hart Lane"
},
{
"name": "Arsenal F.C.",
"league": "Premier League",
"points": 52,
"won": 15,
"drawn": 7,
"lost": 7,
"gf": 46,
"ga": 30,
"founded": 1886,
"arena": "Emirates Stadium"
},
{
"name": "Manchester City F.C.",
"league": "Premier League",
"points": 51,
"won": 15,
"drawn": 6,
"lost": 8,
"gf": 52,
"ga": 31,
"founded": 1880,
"arena": "City of Manchester Stadium"
}
]
},
"links": {
"prev": "http://api.footballteams/resources/teams/?page=1&per_page=4",
"next": "http://api.footballteams/resources/teams/?page=2&per_page=4",
"first": "http://api.footballteams/resources/teams/?page=1&per_page=4",
"last": "http://api.footballteams/resources/teams/?page=5&per_page=4",
}
}
Then, we have defined team
model as follows:
// app/models/team.js
import DS from 'ember-data';
const { attr } = DS;
export default DS.Model.extend({
name: attr('string'),
league: attr('string'),
points: attr('number'),
won: attr('number'),
drawn: attr('number'),
lost: attr('number'),
goalsAgainst: attr('number'),
goalsFor: attr('number'),
founded: attr('number'),
stadium: attr('string')
});
Let's write our custom normalizeResponse
method to get rid of some useless attributes:
// app/serializers/team.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
primaryKey: 'name',
normalizeResponse(store, model, payload, id, requestType) {
// deletes useless 'result' attribute
delete payload.result;
// removes data attribute and keep teams
payload.teams = payload.data.teams;
delete payload.data;
// looks like this endpoint requires pagination
payload.links = doSomethingToHandlePagination();
delete payload.links
return this._super(...arguments);
}
});
API Response does not include an id for each team
, so we are setting name
as an identifier for each record. Nevertheless, using the team's name
as id
is probably an anti-pattern; you could try to generate an id for each record in the serializer, but that's beyond the scope of this post.
Important
As we're using normalizeResponse
, this normalization will apply to any request related to the team
model. If your API structure isn't consistent and varies for each endpoint (GET api/teams/{team_id}
, POST api/teams
, PUT api/teams/{team_id}
, etc), you should try with normalizeFindAllResponse
, normalizeCreateRecordResponse
or any normalize*
method that matches your API request. For example, the code above could have been written using the normalizeFindAllResponse
method as follows:
// app/serializers/team.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
primaryKey: 'name',
normalizeResponse(store, model, payload, id, requestType) {
// deletes useless 'result' attribute
delete payload.result;
return this._super(...arguments);
},
normalizeFindAllResponse(store, model, payload, id, requestType) {
// removes data attribute and keep teams
payload.teams = payload.data.teams;
delete payload.data;
// looks like this endpoint requires pagination
payload.links = doSomethingToHandlePagination();
delete payload.links
return this._super(...arguments);
},
});
Our payload it's almost normalized, but we still need to match ga
, gf
, arena
attributes with the team
model attributes goalsAgainst
, goalsFor
and stadium
, respectively. Let's add a custom normalize
method to our team
serializer:
// app/serializers/team.js
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizeResponse(store, model, payload, id, requestType) {
// ... as defined above
},
normalize(model, hash, prop) {
hash.goalsAgainst = hash.ga;
delete hash.ga;
hash.goalsFor = hash.gf;
delete hash.gf;
hash.stadium = hash.arena;
delete hash.arena;
return this._super(model, hash, prop);
},
});
The normalize
method will be called for each team
within the API response. Now, the payload fully matches what's expected by Ember Data and the team
model will get populated.
The methods described above work for Ember Data 1.13 - 2.4.
The slides are available here.