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
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.
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
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:
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
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:
1 2 3 4 5 6
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:
Package will be handled using
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Serializers are just classes which inherit from
ActiveModel::Serializer. You can define regular methods and the model being serialized is accessed by
And how does the code inside
UsersController look like? It’s here:
1 2 3 4 5 6 7 8 9
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
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!
1 2 3 4 5
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
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
That’s an infinite loop: user contains package, package contains user, user contains package…
How can we avoid this?
1 2 3 4 5 6 7
1 2 3 4 5 6 7 8 9 10
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.
1 2 3 4 5 6 7
1 2 3
: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
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.
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.
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).