Ruby
Article

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

By Hendra Uzia

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.

More:
  • Fred@Bootstrap

    By some strange coincidence I just started working on rolling my own model JSON-izer on a 3-resource combination response when I saw this! I’ll now give it a go using the AM Serializer instead. Maybe paste the code and change the names a little :D Cheers Glenn!

    • ggsp

      Ah! I didn’t write this, Hendria Uzia did. I published it under myself on accident! Sorry Hendria!

      I am glad you like it, though, Fred!

      • http://Post20.com theresa_lee7

        This is something very interesting that is worth paying your extreme attention ,a very good chance to work for those people who want to use their free time so that they can make some extra money using their computers… I have been working on this for last two and half years and I am earning 60-90 dollar/ hour … In the past week I have earned 13,70 dollars for almost 20 hours sitting ….

        Any special qualification, degree or skills is not necessary for this, just keyboard typing and a good working and reliable internet connection ….

        Not any Time limitations to start work … You may do this work at any time when you willing to do it ….

        Just know how I have been doing this…..….see this (webiste-Iink) on my !profile!` to know how I am working` on this`

        !1q

      • Fred@Bootstrap

        In this case, cheers Hendria! :)

  • http://Post20.com Deborah Boyer

    This is something very interesting that is worth paying your extreme attention ,a very good chance to work for those people who want to use their free time so that they can make some extra money using their computers… I have been working on this for last two and half years and I am earning 60-90 dollar/ hour … In the past week I have earned 13,70 dollars for almost 20 hours sitting ….

    Any special qualification, degree or skills is not necessary for this, just keyboard typing and a good working and reliable internet connection ….

    Not any Time limitations to start work … You may do this work at any time when you willing to do it ….

    Just know how I have been doing this…..….see this (webiste-Iink) on my !profile!` to know how I am working` on this`

    #$%HG

  • http://www.iamthemangosteen.com Jeffrey Wan

    Is there an original codebase I can use?

  • Agnaldo Junior

    Very good tutorial!

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.