Active Model Serializers, Rails, and JSON! OH MY!

来源:转载

JSON (JavaScript Object Notation) is a format that can be used to store or exchange data. It is easy to read by humans and easy to parse by machines, which is why a lot of APIs use JSON.

In this article, we will learn how to create custom JSON responses with ActiveModel::Serializer . All examples are created using a Ruby on Rails application. Creating JSON responses in Rails is easy, but using the framework default feature is not enough and is not easily testable.

Consider the following code:

render json: user

The above code will create a JSON response that consists of all user attributes. There are a number of options that you can use with it, such as include , only , except , and methods , but in a real application it needs more than what the default approach can give.

In this article, we will learn how to create and manage serializers for models with the following relationships.

We will be using a fictitious application for this article, the above models will be rendered as a response for the following API endpoints:

In the following sections, we will learn how to manage serializers using several constraints for good maintainability. Also, I’ll show multiple strategies on how to embed data and provide link discovery to increase application performance.

This article will only discuss managing and implementing serializers. It will not discuss other implementation details that are required to achieve a working application. The final application is available on Github as a companion to this article.

Introduction to ActiveModel::Serializer

ActiveModel::Serializer provides a way of creating custom JSON by representing each resource as a class that inherits from ActiveModel::Serializer . With that in mind, it gives us a better way of testing compared to other methods. It can also be tested in isolation regardless of how the data retrieval is done in the controller.

I am using ActiveModel::Serializer version 0.9.3. The strategies displayed in this article are not specific to this version, nor to this gem. It is supported in the previous version, and will still be supported in the future version. However, implementation details might be different for each version, please consult the documentation for different versions.

Installation

Add the following gem to your Gemfile :

gem 'active_model_serializers', '0.9.3'

Then install it using bundle:

bundle install

That’s it, the installation is done.

Usage

You can generate a serializer as follows:

rails g serializer user

The above generator will create a serializer in app/serializers/user_serializer.rb with the following content:

To gain an understanding of how it works, let’s implement the serializers focusing on our use case. Assuming we already have all our models in place, we can create serializers for our model either manually or using the generator.

Serialization Constraints

Before digging into the detail of implementing various strategies of rendering custom JSON responses, we need to have a firm grasp of how to manage serializers. It is very easy to create complex JSON responses using ActiveModel::Serializer , but it’s strongly discouraged.

The following examples use basic constraints that you can follow. They should serve the goal of simple and maintainable serializers.

Models

Each model should have an accompanying serializer with attributes required by the client. With the above use case, we should have the following serializers:

It is possible to automatically include all attributes of a model, but it will introduce the risk of accidentally sending sensitive data to the client. It also floods the client with unnecessary data. This is strongly discouraged unless the model has a small attribute set and they rarely change.

In that case, the serializer below (not related to our use case, but just used for this example) serves the purpose of a simple serializer example that is includes all attributes automatically:

Each endpoint requires a new serializer. These serializers are different from the above serializers. Each endpoint has a dedicated serializer to reduce the dependency for a given serializer and to increase maintainability by introducing a one-to-one relationship between endpoint and serializer.

Here are the serializers required by the endpoints in our use case. We can use inheritance to generalize attributes across other serializers of a given model:

There’s an interesting statement in the serializer for the API user profile. If you do not set the root name of that serializer it will be set to show . If you don’t need a root, you can set the root to false .

Embedding Data

Embedding data is a way of including references or data related to the object being requested. ActiveModel::Serializer provides two kinds of embedding data, embedding a single object or embedding a collection. The method is similar to adding a relationship to an ActiveRecord model. Let’s take a look at the following example:

While it is possible to do the above, it is strongly discouraged. You can accidentally send an very large ponse using has_many . Instead of doing the above method, split the request into two end points using two serializers. Therefore, we will have the following serializers:

A serializer that requires loading a lot of children should be split into multiple serializers. This way it can be maintained easily and avoid unnecessary risks of accidentally sending a huge amount of data to the client.

Embedding and Link Discovery

There are multiple strategies that you can use to render custom JSON responses. They involve embedding data and link discovery. Each of the following strategies has its own benefits and drawbacks. They are suitable for different purposes, and should be used appropriately.

JSON response strategies to embed data can be done using nested data or sideloading the data. While a strategy to provide link discovery can be done using HATEOAS -based JSON responses.

JSON Response with Nested Data

Nested data enables the client to load all referenced data at once, therefore we can reduce the number of calls required to load all data. This strategy doesn’t require data processing, and can be used immediately to display the data. The drawback of this strategy is that it can introduce data duplication. The same data may be nested in multiple objects.

In order to do this, you need to include the data that needs to be nested by defining its relationship. Using previously defined serializers for each endpoint, define the relationship inside the serializer:

Sideloading data enables a client to load all referenced data, therefore we can reduce multiple requests required to load all data to only one request. The benefit of using this approach is no data duplication introduced in the response. The drawback of this strategy is that it requires the client to process data in some way before displaying it.

In order to generate side-load data, we need to do the same thing as we have done in the previous section. Then, we need to set it only to embed the ids and set include to true.

HATEOAS stands for Hypertext As The Engine Of Application State. HATEOAS enables a client to interact entirely through the provided hypermedia format by the server. It means that the hypertext itself can be used to navigate an API. Unfortunately, JSON is not a hypermedia format.

Although JSON has no hypermedia support, we can still provide a way of link discovery in JSON. To serve a HATEOAS-based JSON response, implement the link discovery in JSON as follows:

The benefit of using this approach is that the client can easily interact with the API using the links in the provided response. But the drawback to this approach is that there is no standard for HATEOAS-based JSON responses, and, therefore, the client implementation is tightly coupled with the response format.

There are alternative to HATEOAS-based JSON formats, such as JSON-LD and JSON API . They provide features like link discovery, but require a totally different implementation on the client.

More from this author JSON Validation by Committee Document Your JSON API Schema with PRMD Conclusion

Rendering custom JSON response using ActiveModel::Serializer requires discipline and consistency. Failing to stick to these goals can reduce application maintainability and client performance. This article should provide you a basic understanding of how to render custom JSON responses using different strategies that suit you best.

Alternatives such as Jbuilder , Grape , and RABL have different approaches to rendering the response. To have a better understanding of why you should use ActiveRecord::Serializer , you should check out these alternatives, as well.