Making Magic in Rails JSON APIs

Tricks and caveats when using ActiveModel::Serializer

Posted on May 12, 2015

As almost everything, making an JSON API in Rails is something very smooth. However, such sugar can become sour sometimes. Think about how to change model-attribute names in JSON objects easily ? .. or to combine many models in a same response or to return collections ?? Finally, how to address all these questions keeping your code beautiful and clean ???

ActiveModel::Serializer provides magic ways to serializer your model objects into different JSON objects. Moreover, it helps to keep your code DRY and organized, in terms of what Business Resources your API is in charge of.

Creating a Serializer

Creating a simple serializer is something trivial. After adding the active_model_serializers gem, you get started by creating your serializer:

$ rails g serializer UserPreview

Then, you choose which attributes you want to serialize:

class UserPreviewSerializer < ActiveModel::Serializer
  attributes :id, :avatar_url, :name, :age

  def name
    "#{object.first_name} #{object.last_name}"
  end
end

In this example, :id and :avatar_url are model attributes while :age is a model method. We can notice that :name is not a model attribute nor a model method, it's a serializer method that combines two model attributes.

Finally, you have to set up which serializer to use when rendering json objects:

class Api::UsersController < BaseController
  def show
    user = User.find(param[:id])
    render json: user, serializer: UserPreview
  end
end

Combining serializers

Another advantage of using ActiveModel::Serializer is to combine different models, through their serializers, into a same JSON response. Let's say a user is booking a hostel, or even better, he/she intends to exchange his skills for accommodation in a nice hostel ( Worldpackers.com ).

The API returns some information about the booking. In this case, the response may include many models (User, Booking, Message, Order).

class BookingSerializer < ActiveModel::Serializer
  attributes :id, :arrival, :departure, :last_message

  has_one :user, serializer: UserPreviewSerializer
  has_one :last_message, serializer: MessageSerializer

  def last_message
    object.messages.order(created_at: :desc).first
  end
end

In this example, we're combining booking attributes and other 2 serializers. Since, we want to return all user bookings, we render our JSON invoking the each_serializer statement:

render json: bookings, each_serializer: BookingSerializer, root: false 

We defined root: false. It means that the response will not include the serializer name (booking) in the root of our JSON object. As a result, our API will return:

[
  {
    id: 42,
    arrival: "1710-07-10",
    departure: "1710-07-25",
    last_message: {
      content: "You know nothing John Snow",
      created_at: "1710-07-24 15:42"
    },
    user: {
      id: 41112,
      name: "Ygritte",
      avatar_url: "http://images/got-42.jpg",
      age: 23
    }
  }
]

Caveats and tricks

When adding ActiveModel::Serializer to your project, Rails starts to use it to serializing all json responses. Depending the version of ActiveModel::Serializer you are using, root: false will be the default behavior. So, be careful to don't break other API responses.

Organize your serializer to meet business rules. In our example, UserPreviewSerializer is in the response because we only need a preview. However, in a context where you need more user information, we can create something like a UserProfileSerializer or CompleteUserSerializer.

That's it! Thank you for reading! Long live the ActiveModel::Serializer