Adam Niedzielski

Solve business problems with high quality code

JSON API with rails-api and active_model_serializers

| Comments

This post is based on my presentation given at Lodz Ruby Users Group. The slides are here (in Polish).

Rails and JSON

Dealing with JSON in Rails is pretty easy and straightforward. The support is built into the framework, request data is automatically available in params hash. Then we just have to say render :json and we are done.

This may be sufficient if JSON is one of the formats which we use. For instance, we can render both JSON and HTML in the same controller action. Or we may create an action intended for AJAX call which changes some model and returns JSON with status field.

On the other hand, we may use Ruby on Rails for building JSON API – app with the assumption that JSON is the only format we support. In such a case we know that the app won’t render HTML. There is no View layer (or you can say that JSON is your View layer). We probably won’t need cookies support and the code specific to handling browser requests.

rails-api

Ruby on Rails has modular structure – you can opt out of the things which you don’t want to use. It’s described thoroughly in this wonderful blog post. But if we are talking about API-only Rails app somebody already did it for us and published in the gem called rails-api.

Why should you care?

My personal reason is: “don’t require things which you won’t use”. There might be bugs hiding there or security vulnerabilities connected with it. And of course there are other reasons:

  • more lightweight application

I generated two fresh applications – one using rails-api new and the second using rails new. The environment was consistent – Ruby 2.1.0, Rails 4.0.2. Then I started the server with rails s and measured RAM memory taken by server process. The results are following:

37.4 MB vs. 43.8 MB for rails-api

This is 15% less. Obviously when you start adding new gems the relative difference will be smaller.

  • faster application

The same benchmarking environment as above. I created controller action which loads 100 User records from the database and renders them as JSON. I placed exactly the same code in rails-api application and regular rails application. Then I measured the server response time for a bunch of requests and took the avarage value. It looks like:

31.0 ms vs. 35.2 ms for rails-api

This is 12% faster.

  • useful generator

The controller scaffold which comes with rails-api is really cool. It disables new and edit actions (they don’t make sense because we do not display forms) and provides sensible default code inside other actions.

  • easy installation and no maintenance costs

Last but not least, rails-api is super easy to install and learn. All you have to do to bootstrap new app is:

1
2
gem install rails-api
rails-api new my_app

What’s more, you can re-enable some modules disabled by rails-api or just completely remove rails-api without any problem. rails-api is not a burden. It’s worth to spend 10 minutes of your time learning it. Check out the docs

active_model_serializers

Update 26.05.2015

This description talks about version 0.8.x of active_model_serializers!

If you want to build JSON API it’s good to have control over the generated JSON. And here we need the second gem – active_model_serializers.

Disclaimer: there are other gems meant to solve similar problem, for example rabl and Jbuilder. I haven’t used them in a project yet, so I can’t make any comparisons. But active_model_serializers works for me.

Let’s look at a sample serializer:

user_serializer.rb
1
2
3
4
5
6
class UserSerializer < ActiveModel::Serializer
  attributes :id, :first_name, :last_name, :email

  has_one :address
  has_many :packages
end

Serializers live in app/serializers directory. As you can guess UserSerializer is used to serialize User model. Using attributes we define what should be included in the generated JSON. You may think that this is repeating yourself (the fields are already defined in database schema), but in my opinion you rarely render all the fields, more often you want to hide some internals.

As you can see we embed associated records using familiar syntax: has_one and has_many. Serializing Address and Package will be handled using AddressSerializer and PackageSerializer, respectively.

No magic

user_serializer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserSerializer < ActiveModel::Serializer

  [...]

  attributes :full_name, :email_address

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

  def email_address
    object.email
  end

  [...]
end

Serializers are just classes which inherit from ActiveModel::Serializer. You can define regular methods and the model being serialized is accessed by object method.

And how does the code inside UsersController look like? It’s here:

users_controller.rb
1
2
3
4
5
6
7
8
9
[...]

  def index
    @users = User.includes(:address, :packages)

    render json: @users
  end

[...]

Pretty simple, isn’t it? You just say: “I want JSON” and you have JSON rendered with proper serializer.

The generated JSON will look similar to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "users":
    [
      {
        "id": 1,
        "first_name": "Some String",
        "last_name": "Another String",
        [...]
        "address": { "street": "Yet another string" },
        "packages": [
          { "id": 2, "status": "delivered" },
          { "id": 5, "status": "lost" }
        ]
      }
    ]
}

More about associations

Let’s imagine following use case: we want to get information about a package given its id. And we want it to contain information about the owner (user). Difficult? Not really!

package_serializer.rb
1
2
3
4
5
class PackageSerializer < ActiveModel::Serializer
  attributes :id, :status

  has_one :user
end

What’s different here from model code? Package belongs_to user, but here it says has_one user. From the point of view of serializers belongs_to is exactly the same as has_one, hence we have only has_one and has_many.

Now let’s go back to UsersController#index after our changes. We hit the action again and what do we get this time?

1
2
3
 Failure/Error: Unable to find matching line from backtrace
 SystemStackError:
   stack level too deep

That’s an infinite loop: user contains package, package contains user, user contains package…

How can we avoid this?

Solution #1

user_serializer.rb
1
2
3
4
5
6
7
class UserSerializer < ActiveModel::Serializer
  [...]

  has_many :packages, :embed => :ids

  [...]
end
1
2
3
4
5
6
7
8
9
10
{
  "users":
    [
      {
        "id": 1,
        [...]
        "package_ids": [2, 5]
      }
    ]
}

We can use :embed option and include only ids of the packages. This has a drawback: if a client of our API wants not only id of the package, but also its status then he will have to make a separate request for each package. Certainly this is a situation which we want to avoid.

Solution #2

user_serializer.rb
1
2
3
4
5
6
7
class UserSerializer < ActiveModel::Serializer
  [...]

  has_many :packages, :serializer => ShortPackageSerializer

  [...]
end
short_package_serializer.rb
1
2
3
class ShortPackageSerializer < ActiveModel::Serializer
  attributes :id, :status
end

We use :serializer option to specify different serializer than that inferred from naming conventions. We create ShortPackageSerializer, which doesn’t contain user embeded. What’s more you can put ShortPackageSerializer and PackageSerializer in the inheritance hierarchy so you DRY.

In my opinion this solution is pretty clean. We have separate class for each representation and we are able to share some pieces of code by using inheritance. Of course, this may become too complex if the inheritance hierarchy grows very big. However if we limit ourselves to 2-3 serializers per model the code should stay clear and maintainable.

Summary

Use rails-api if you are building API-only application. It’s easy to learn and maintenance won’t hit you later, because you can opt out without any problems.

Use active_model_serializers for better control over the generated JSON.

Sample application

You can find my sample application here. It’s a university project, so don’t expect it to be a complete solution for any problem. However, it contains examples for everything which I covered above (plus some extra things like specs for the API).

Comments