jbuilder vs active_model_serializers vs oj_serializers - Rails JSON API Comparison
Rails provides a built-in as_json method for generating JSON. Additionally, jbuilder is the default gem for handling JSON APIs, and there are other alternative gems available. This article compares the following solutions:
Based on personal experience, the evaluation considers the following criteria:
- Ease of maintenance.
- Reusability of defined structures.
- Ability to generate JSON objects and use them within the code.
- Performance.
- Ease of testing.
Scenario
Assume we have two models: Post and User. The goal is to generate the following JSON outputs:
Post index
1 | { |
Post show
1 | { |
User index
1 | { |
This scenario simulates displaying partial data on an index page and showing complete data with nested structures on a detailed page.
Implementation
The following sections demonstrate how to implement the example using four different approaches.
as_json
post.rb
1 | class Post < ApplicationRecord |
posts_controller.rb
1 | class PostsController < ApplicationController |
users_controller.rb
1 | class UsersController < ApplicationController |
Using the built-in as_json function allows simple conversion of model data into JSON. However, it’s not easy to handle fields that need processing. In the example above, you can see that timeago needs to be implemented in the model. Additionally, reusing structures requires custom design; otherwise, it will result in a lot of redundant code, as seen in the example.
jbuilder
users/_user.json.jbuilder
1 | json.extract! user, *%i[id name] |
users/index.json.jbuilder
1 | json.users do |
posts/_post.json.jbuilder
1 | json.extract! post, *%i[id title] |
posts/index.json.jbuilder
1 | json.posts do |
posts/show.json.jbuilder
1 | json.post do |
posts_controller.rb
1 | class PostsController < ApplicationController |
users_controller.rb
1 | class UsersController < ApplicationController |
jbuilder uses a view-rendering approach for generating JSON, moving JSON construction logic into views. Partial templates allow structure reuse. However, passing parameters is necessary to control different outputs, as seen in the Post show example.
Since JSON is generated using Render View, producing JSON outside of an API becomes more complicated. Additionally, testing must be conducted through a controller, and code within the view is excluded from code coverage.
If you want to generate JSON using the existing view in your code, you can write it like this:
1 | ActionController::Base.new.render_to_string( |
This generates a JSON string. If you need an object for further operations, you must first use JSON.parse.
active_model_serializers
user_serializer.rb
1 | class UserSerializer < ActiveModel::Serializer |
post_serializer.rb
1 | class PostSerializer < ActiveModel::Serializer |
post_detail_serializer.rb
1 | class PostDetailSerializer < PostSerializer |
posts_controller.rb
1 | class PostsController < ApplicationController |
users_controller.rb
1 | class UsersController < ApplicationController |
active_model_serializers uses the defined Serializer class to output data. Reusable parts can also be output in detail by using inheritance. Like the PostDetailSerializer above.
If you want to generate JSON in the code, you can write it like this:
1 | ActiveModelSerializers::SerializableResource.new(post, { |
However, there are actually quite a few issues with its usage:
nil requires special handling
When retrieving a single record, if the expectation is to return null instead of a 404, an error occurs. For example, in the following case:
1 | render json: post, serializer: PostDetailSerializer |
When post is nil, the expectation is to output:
1 | { |
Actually, an error occurs.
1 | undefined method `read_attribute_for_serialization' for nil |
It turns out it needs to be written like this:
1 | if post |
Default single root
The default behavior automatically generates a root like posts or post, for example:
1 | { |
To output additional data, for example:
1 | { |
The default behavior can’t achieve this. You can either use the JSON generation method mentioned above or use the meta parameter.
1 | render json: Post.all, meta: { total_pages: 10 } |
However, this will add an extra level of nesting.
1 | { |
oj_serializers
user_serializer.rb
1 | class UserSerializer < Oj::Serializer |
post_serializer.rb
1 | class PostSerializer < Oj::Serializer |
post_detail_serializer.rb
1 | class PostDetailSerializer < PostSerializer |
posts_controller.rb
1 | class PostsController < ApplicationController |
users_controller.rb
1 | class UsersController < ApplicationController |
Similar to active_model_serializers, it uses class definitions to generate output, but it’s more intuitive to use. By calling the functions defined in the Serializer, you can directly generate JSON. In the controller, you can easily assemble the JSON, and pagination or other information can be added easily. As for the nil issue with a single record, if you want to output nil, you can achieve it using the one_if function:
1 | render json: { post: PostDetailSerializer.one_if(post) } |
Benchmark
Next, let’s test the performance. I created a benchmark project, and the results are approximately as follows:
1 | as_json 0.116 (± 0.0%) i/s (8.62 s/i) - 1.000 in 8.623844s |
oj_serializer has the best performance, and I also happened to find that adding to_json further improves the performance. Using the example above, it would be modified like this:
1 | render json: { posts: PostSerializer.render(Post.all) }.to_json |
All the tests above have already included the optimization results from oj.
Conclusions
We compare the requirements in a table:
| Requirement | as_json | jbuilder | active_model_serializers | oj_serializer |
|---|---|---|---|---|
| Ease of maintenance | X | O | O | O |
| Reusability | X | O | O | O |
| Generate JSON objects | O | △ | O | O |
| Performance | △ | △ | X | O |
| Ease of testing | O | △ | O | O |
oj_serializer is a great choice.

