Ember Data and Custom APIs - 5 Common Serializer Customizations

Saturday,
January
23rd,
2016

When I first started working with Ember Data in the 1.X days, one of the most frustrating things was having to work with custom APIs. This wasn’t because of Ember Data but because I was new to it and there is a lot to learn. I was motivated to use Ember Data but I just couldn’t get it to work with whatever API I was using at the time. Do I massage the data in an adapter or a serializer? What is the difference between normalizeReponse() and normalize() in serializers? How do I handle related data that is nested? I had lots of questions like these. Even today I see a lot of the same questions being asked on the Ember Discussion Forum about getting Ember Data to work with custom APIs. In this post, I’d like to share a few common ways to customize serializers that others might be wanting to make as well, especially those new to the framework.

If your API is built with Rails, then it’s likely it is following the conventions expected by Ember Data and everything just works. For those who don’t have control over the API, some customizations might be needed to manipulate responses before they are handed off to the data store or data is sent back to the server. This data massaging happens in the serializer layer.

A serializer in Ember Data is used to massage data as it is transferred between the client and the persistence layer. This includes manipulating attribute values, normalizing property names, serializing relationships, and adjusting the structure of request payloads and responses.

This format is pretty similar to the RESTSerializer format, except the root key is data as opposed to the model name. One way we can manipulate this is by creating a model specific serializer that extends RESTSerializer and override normalizeResponse():

The normalizeResponse() method is used to normalize a payload from the server to a JSON-API document (the JSON API spec). Rather than creating and returning the JSON-API document directly, we can instead modify the payload to fit the RESTSerializer conventions, that is, a payload with a root key containing the model name, and calling this._super() which will return a JSON-API compliant document. I have found this approach to be simpler than massaging the data to fit the JSON-API format directly.

Another way we could have handled this is by using the JSONSerializer.

Because JSONSerializer expects the payload to contain the data without any root keys, we can simply extract that data property.

Similar to normalizeResponse(), serializers also have methods that match specific data store calls. For example, if you want to normalize data only when store.findAll() is called, you can use normalizeFindAllResponse() instead.

To normalize only a single model for an endpoint such as /cats/1, use the normalize() method.

Each cat has a belongsTo relationship to another model called breed. The related data is nested within each cat object as opposed to being sideloaded. In its current format, the breed relationship won’t be setup properly. Let’s change that by overriding normalizeResponse():

By extending RESTSerializer, we can extract breed from each cat and create a payload where the related data is sideloaded under the key breeds. We also need to modify breed on each cat so that it equals breed.id as opposed to the full breed object.

So back to the question, should you extend RESTSerializer or JSONSerializer? If you have nested models, extend RESTSerializer so that you can normalize the related data to be sideloaded.

3. Mapping Attributes to Model Properties

Many APIs return properties that are snake_cased:

[{"id":1,"first_name":"Tubby","years":4}]

Although you could work with snake_cased attributes in your app, this isn’t the JavaScript convention. If you’d like to map these to camelCased properties, or map any attribute to another property on your model for that matter, you can specify this mapping in the attrs attribute.

first_name gets mapped to firstName and years gets mapped to age on your model.

Note there is an ember addon to help automate mapping snake_cased attributes to camelCased attributes called
Ember Data ActiveModel Adapter that you might find useful.

4. Relationship Attributes

Using the attrs attribute can be useful if you want to map specific attributes. But what if your API follows a convention of xxx_id for every belongsTo relationship? This is a pretty common convention in relational databases that you end up seeing in APIs. For every model you’d have to create a serializer and specify the attribute mappings in attrs. Instead, a better way might be to override the method keyForRelationship in an application serializer.

Here we are changing the id attribute to be socialSecurityNumber instead for the user model. When you want to access socialSecurityNumber throughout your application, you can do:

model.get('id');

If all of your records use a property like _id as the id, then you might want to override primaryKey in an application serializer. If you want to override id for a specific model such as in this example, then you might want to create a model specific serializer.

Conclusion

There are different takes on what API payloads should look like and they won’t always follow the formats expected by the default Ember Data serializers. Hopefully this post helps reduce some of the frustration that I experienced when I first started using Ember Data with custom APIs. What serializer customizations have you made? Let me know in the comments!