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

Share this article

JavaScript Object Notation concept.

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.

models-large

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

  • Retrieve the latest videos
  • Retrieve the user profile
  • Retrieve a video’s latest comments

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:

# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
end

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:

# app/serializers/video_serializer.rb
class VideoSerializer < ActiveModel::Serializer
  attributes :id, :title, :description
end

# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name
end

# app/serializers/comment_serializer.rb
class CommentSerializer < ActiveModel::Serializer
  attributes :id, :text
end

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:

# app/serializers/tag_serializer.rb
class TagSerializer < ActiveModel::Serializer
  attributes *Tag.column_names
end

End Points

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:

# serializer for API latest videos
# app/serializers/videos/index_serializer.rb
class Videos::IndexSerializer < VideoSerializer
end

# serializer for API user profile
# app/serializers/users/show_serializer.rb
class Users::ShowSerializer < UserSerializer
  root 'user'
end

# serializer for API video's latest comments
# app/serializers/comments/index_serializer.rb
class Comments::IndexSerializer < CommentSerializer
end

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:

# app/serializers/videos/index_serializer.rb
class Videos::IndexSerializer < VideoSerializer
  has_one :user

  # WARNING:
  # The following is for example purposes only, try to avoid at all costs.
  has_many :comments
end

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:

# app/serializers/videos/index_serializer.rb
class Videos::IndexSerializer < VideoSerializer
  has_one :user
end

# app/serializers/comments/index_serializer.rb
class Comments::IndexSerializer < CommentSerializer
  has_one :user
end

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:

# app/serializers/videos/index_serializer.rb
class Videos::IndexSerializer < VideoSerializer
  has_one :user
end

# app/serializers/users/show_serializer.rb
class Users::ShowSerializer < UserSerializer
  root 'user'
end

# app/serializers/comments/index_serializer.rb
class Comments::IndexSerializer < CommentSerializer
  has_one :user
end

The above serializers will generate json responses as follows.

Retrieve Latest Videos

{
  "videos":[
    {
      "id":135801055,
      "title":"Les quatre cents coups",
      "description":"Moving story of a young boy who, left without attention, delves into a life of petty crime.",
      "user":{
        "id":321975000,
        "name":"Sarah Ferguson"
      }
    },
    {
      "id":419895383,
      "title":"La heine",
      "description":"24 hours in the lives of three young men in the French suburbs the day after a violent riot.",
      "user":{
        "id":321672063,
        "name":"Javier Dean"
      }
    }
  ]
}

Retrieve User Profile

{
  "user":{
    "id":1066742030,
    "name":"Jill Ray"
  }
}

Retrieve Video’s Latest Comments

{
  "comments":[
    {
      "id":632418569,
      "text":"All these jokes are purely jokes - nothing is meant seriously.",
      "user":{
        "id":623761651,
        "name":"Terry Holland"
      }
    }
  ]
}

JSON Response with Sideloaded Data

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.

# app/serializers/videos/index_serializer.rb
class Videos::IndexSerializer < VideoSerializer
  embed :ids, include: true
  has_one :user
end

# app/serializers/users/show_serializer.rb
class Users::ShowSerializer < UserSerializer
  root 'user'
end

# app/serializers/comments/index_serializer.rb
class Comments::IndexSerializer < CommentSerializer
  embed :ids, include: true
  has_one :user
end

The above serializers will generate json responses as follows.

Retrieve Latest Videos

{
  "videos":[
    {
      "id":135801055,
      "title":"Les quatre cents coups",
      "description":"Moving story of a young boy who, left without attention, delves into a life of petty crime.",
      "user_id":321975000
    },
    {
      "id":419895383,
      "title":"La heine",
      "description":"24 hours in the lives of three young men in the French suburbs the day after a violent riot.",
      "user_id":321672063
    }
  ],
  "users":[
    {
      "id":321975000,
      "name":"Sarah Ferguson"
    },
    {
      "id":321672063,
      "name":"Javier Dean"
    }
  ]
}

Retrieve User Profile

{
  "user":{
    "id":1066742030,
    "name":"Jill Ray"
  }
}

Retrieve Video’s Latest Comments

{
  "comments":[
    {
      "id":632418569,
      "text":"All these jokes are purely jokes - nothing is meant seriously.",
      "user_id":623761651
    }
  ],
  "users":[
    {
      "id":623761651,
      "name":"Terry Holland"
    }
  ]
}

HATEOAS-based JSON Response

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:

# app/serializers/video_serializer.rb
class VideoSerialzer < ActiveModel::Serializer
  attributes :id, :title, :description, :links

  def links
    { self: video_path(object.id) }
  end
end

# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :links

  def links
    { self: user_path(object.id) }
  end
end

# app/serializers/comment_serializer.rb
class CommentSerializer < ActiveModel::Serializer
  attributes :id, :text, :links

  def links
    { self: comment_path(object.id) }
  end
end

The above serializers will generate json responses as follows.

Retrieve Latest Videos

{
  "videos":[
    {
      "id":135801055,
      "title":"Les quatre cents coups",
      "description":"Moving story of a young boy who, left without attention, delves into a life of petty crime.",
      "links":{
        "self":"/videos/135801055"
      },
      "user_id":321975000
    },
    {
      "id":419895383,
      "title":"La heine",
      "description":"24 hours in the lives of three young men in the French suburbs the day after a violent riot.",
      "links":{
        "self":"/videos/419895383"
      },
      "user_id":321672063
    }
  ],
  "users":[
    {
      "id":321975000,
      "name":"Sarah Ferguson",
      "links":{
        "self":"/users/321975000"
      }
    },
    {
      "id":321672063,
      "name":"Javier Dean",
      "links":{
        "self":"/users/321672063"
      }
    }
  ]
}

Retrieve User Profile

{
  "user":{
    "id":1066742030,
    "name":"Jill Ray",
    "links":{
      "self":"/users/1066742030"
    }
  }
}

Retrieve Video’s Latest Comments

{
  "comments":[
    {
      "id":632418569,
      "text":"All these jokes are purely jokes - nothing is meant seriously.",
      "links":{
        "self":"/comments/632418569"
      },
      "user_id":623761651
    }
  ],
  "users":[
    {
      "id":623761651,
      "name":"Terry Holland",
      "links":{
        "self":"/users/623761651"
      }
    }
  ]
}

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.

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.

Frequently Asked Questions (FAQs) about Active Model Serializers in Rails

What is the main purpose of using Active Model Serializers in Rails?

Active Model Serializers (AMS) in Rails is a flexible and efficient library for serializing your objects into JSON. It provides a way to control how complex Ruby objects are converted to JSON, allowing you to choose what attributes and relationships to include, rename keys, and manipulate attribute values. This is particularly useful when building APIs, as it allows you to tailor the JSON output to the needs of the client application, improving performance and reducing bandwidth usage.

How do I install and set up Active Model Serializers in my Rails application?

To install Active Model Serializers, add the gem to your Gemfile with gem 'active_model_serializers' and run bundle install. To set it up, you need to generate a serializer for each model you want to serialize. You can do this with the command rails g serializer ModelName. This will create a file in the app/serializers directory where you can specify the attributes and relationships to include in the JSON output.

How can I customize the JSON output of my serializers?

You can customize the JSON output by defining methods in your serializer. For example, if you have a User model with a first_name and last_name attribute, and you want to combine them into a single name attribute in the JSON output, you can define a name method in your UserSerializer that returns object.first_name + ' ' + object.last_name.

How do I handle nested relationships with Active Model Serializers?

You can handle nested relationships by defining associations in your serializers. For example, if you have a Post model that belongs to a User, and you want to include the user’s details in the JSON output for a post, you can add belongs_to :user in your PostSerializer. This will include the user’s attributes as defined in the UserSerializer.

Can I use Active Model Serializers with Rails API-only applications?

Yes, Active Model Serializers is particularly useful for Rails API-only applications. It allows you to control the JSON output of your API, making it easier for client applications to consume your API. You can use it to include only the necessary attributes, rename keys, and handle nested relationships.

How do I version my serializers?

Versioning your serializers can be done by creating separate serializer classes for each version of your API. For example, you can have a V1::PostSerializer and a V2::PostSerializer, each with different attributes and relationships. You can then choose the appropriate serializer in your controller based on the API version requested by the client.

How do I handle errors with Active Model Serializers?

Active Model Serializers does not handle errors by default. However, you can add error handling by overriding the serializable_hash method in your serializer and adding error information to the hash if there are any errors on the object.

Can I use Active Model Serializers with non-ActiveRecord objects?

Yes, Active Model Serializers can be used with any object that includes Active Model::Serialization. This includes ActiveRecord objects, but also other objects that include this module. You just need to define the attributes and read_attribute_for_serialization methods on your object.

How do I test my serializers?

You can test your serializers by creating test cases that check the JSON output for a given object. You can use the as_json method on your serializer to get the JSON output, and then compare it to the expected output.

Can I use Active Model Serializers with Rails engines?

Yes, Active Model Serializers can be used with Rails engines. You just need to make sure that the serializers are loaded by adding require_dependency for each serializer in your engine’s initializer.

Hendra UziaHendra Uzia
View Author

Hendra is the software engineer of vidio.com, the largest video sharing website in Indonesia. He also contributes to the open source community on rspec-authorization and minitest-around.

GlennGjsonrails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week