We had previously talked about the Serialization formats and How Serialization is implemented for storing objects in the relational database in the first two parts of the blog series. This article focuses on the various Serializers that prepare and construct API transferable data in Ruby on Rails.
Serialization in Rails for APIs
What is so special about the data rendered by the APIs? Do we really need to serialize it? The short answer is "YES", we need to send a standardized and formatted response to the API consumers in a way they would be able to parse. Otherwise, it would be difficult for the API consumers to make sense of the transferred data. Since most of the API consumers are JavaScript frameworks, the preferred serialization format for APIs in Rails is JSON.
How to send JSON response from Rails APIs?
For simple use cases, we could use render json: object
in the API controller action. Rails internally applies to_json
to render the response. Alternatively, we could apply the JSON.dump
or to_json
methods on our objects and send it in the response.
Let's say we are building a basic e-learning site. So a user has many courses, which in turn has many topics and each topic has many assignments.
To send all courses of the authenticated user, our courses_controller
's index
action looks somewhat like this:
class Api::V1::CoursesController < ApplicationController
def index
render json: current_user.courses
end
end
This is so easy, isn't it? Or is it? Just think of it, would the API consumer expect us to simply send the title and description of the course? What about the topics, or assignments?
Let us use the include
option for the to_json
method to render the nested objects.
class Api::V1::CoursesController < ApplicationController
def index
render json: current_user.courses.to_json(include: { topics: { include: :assignments } })
end
end
There it is!! We got all the required data in JSON format.
Observe the response. Don't you think sending the created_at
, updated_at
, user_id
etc. in each array were perhaps not needed? Okay, maybe that's not a big deal - we can limit the response attributes by passing only
/ except
options to the to_json
method.
Why do we need JSON serializers?
Well, the real world is not so straightforward as you see. The API consumer would now expect conditional responses, like not sending the assignments data until the corresponding topic has been completed.
We'd then probably create a hash of the course details and render the hash as a JSON object.
class Api::V1::CoursesController < ApplicationController
def index
array_of_courses = current_user.courses.map(&:attributes)
current_user.courses.each_with_index do |course, index|
array_of_topics = course.topics.map(&:attributes)
course.topics.each_with_index do |topic, index|
array_of_topics[index]["is_completed"] = topic.is_completed?(current_user)
array_of_topics[index]["assignments"] = topic.assignments if topic.is_completed?(current_user)
end
array_of_courses[index]["topics"] = array_of_topics
end
render json: array_of_courses
end
end
Doesn't this look very messy? Even with refactoring, the above code is difficult to maintain and has cluttered our controller. For such scenarios instead of manipulating data manually, how about we choose among the various serializers available in the form of gems. These serializers make it easier to build serialized JSON data. They provide wrapper methods that can help us keep the code well-structured and enable customization.
There are different serializer gems available for us to choose from. Out of them we shall explore the four most popular ones and compare them.
Jbuilder
Jbuilder is provided by Rails(>= 5) as the default serializer and is based on the approach that construction of JSON data belongs to the View layer. This gem provides a DSL to customize the responses. The files are placed under the app/views
directory with the extension of .json.jbuilder
. Jbuilder is quite intuitive and helpful when you need to send complex responses with a lot of data manipulations.
class Api::V1::CoursesController < ApplicationController
helper_method :current_user
def index
@courses = current_user.courses
end
end
# /app/views/api/v1/courses/index.json.jbuilder
json.array! @courses do |course|
json.extract! course, :id, :title, :description
json.topics course.topics, partial: 'api/v1/courses/topic', as: :topic
end
end
# /app/views/api/v1/courses/_topic.json.jbuilder
json.extract! topic, :id, :name, :description
if topic.is_completed?(current_user)
json.assignments topic.assignments, partial: 'api/v1/courses/assignment', as: :assignment
end
end
# /app/views/api/v1/courses/_assignment.json.jbuilder
json.extract! assignment, :id, :description
Ruby Benchmark Report
user system total real
0.875491 0.035895 0.911386 (1.000073)
ActiveModel::Serializers:
If you do not wish to use a DSL and are more comfortable with treating serializers like an ActiveRecord object, then you may go with the Active Model Serializers. It is based on Rails conventions for generating data and extracts the serialization logic into a separate folder /app/serializers
.
# /app/serializers/course_serializer.rb
class CourseSerializer < ActiveModel::Serializer
attributes :id, :title, :description
has_many :topics
has_many :user_courses
end
# /app/serializers/topic_serializer.rb
class TopicSerializer < ActiveModel::Serializer
attributes :id, :name, :description
belongs_to :course
has_many :assignments, if: -> { object.is_completed?(current_user) }
attribute :is_completed do
object.is_completed?(current_user)
end
end
# /app/serializers/assignment_serializer.rb
class AssignmentSerializer < ActiveModel::Serializer
attributes :id, :description
belongs_to :topic
end
Now that we have defined the serializers for our models and their associations, all we have to do in the controller action is this:
class Api::V1::CoursesController < ApplicationController
def index
render json: current_user.courses, include: "topics.assignments"
end
end
Please refer to this documentation to know more about how we can include double nested associations.
The result would be something like this:
NEAT, isn't it?
Ruby Benchmark Report
user system total real
0.340994 0.008847 0.349841 (0.483862)
Fast JSON API
Fast JSON API is similar to AMS but is much faster. While AMS lets you configure the adapter to be used, Fast JSON API follows the JSON API specifications.
This serializer is not maintained anymore, but we can use its forked version, i.e the jsonapi-serializer as mentioned in their Github page. I have used FastJsonapi::ObjectSerializer
in the examples below, but we can replace it with JSONAPI::Serializer
too.
The JSON structure may be a bit uncomprehensible specifically for nested relations, but that is how the specifications demand it to be.
# /app/serializers/course_serializer.rb
class CourseSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :title, :description
attribute :topics do |object, params|
TopicSerializer.new(object.topics, { params: { current_user: params[:current_user] } })
end
end
# /app/serializers/topic_serializer.rb
class TopicSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name, :description
has_many :assignments, if: Proc.new { |topic, params| topic.is_completed?(params[:current_user]) }
attribute :is_completed do |topic, params|
topic.is_completed?(params[:current_user])
end
end
# /app/serializers/assignment_serializer.rb
class AssignmentSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :description
end
class Api::V1::CoursesController < ApplicationController
def index
render json: CourseSerializer.new(current_user.courses, { params: { current_user: current_user }, include: [:'topics.assignments'] })
end
end
Ruby Benchmark Report
user system total real
0.468393 0.018134 0.486527 (0.530162)
JSONAPI-RB
Similar to Fast JSON, JSON API offers a bunch of lightweight modules and wrappers to make Ruby applications abide by the JSON-API Specifications
We will use jsonapi-rails
, a gem that provides the required helpers to generate JSON-API conformant responses.
# /app/resources/serializable_course.rb
class SerializableCourse < JSONAPI::Serializable::Resource
type 'courses'
attributes :title, :description
belongs_to :user
has_many :topics
end
# /app/resources/serializable_topic.rb
class SerializableTopic < JSONAPI::Serializable::Resource
extend JSONAPI::Serializable::Resource::ConditionalFields
type 'topics'
attributes :name, :description
belongs_to :course
has_many :assignments, if: -> { @object.is_completed?(@user) }
attribute :is_completed do ||
@object.is_completed?(@user)
end
end
# /app/resources/serializable_assignment.rb
class SerializableAssignment < JSONAPI::Serializable::Resource
type 'assignments'
attributes :description
belongs_to :topic
end
class Api::V1::CoursesController < ApplicationController
def index
render jsonapi: current_user.courses,
expose: { user: current_user },
include: ["topics", "topics.assignments"],
fields: { courses: [:title, :description, :topics],
topics: [:name, :description, :assignments],
assignments: [:description] }
end
end
Ruby Benchmark Report
user system total real
0.418128 0.022805 0.440933 (0.495260)
Which one to choose?
We have seen a few of the many options out there that can be used for serializing API data in Ruby on Rails applications. Many others are not covered here like RABL, Blueprinter, etc. So now the question arises which serializer should you choose for your application?
Here is a good blog that shares reasons and use-cases where a specific serializer would fit best. To summarize that blog, choose Jbuilder if you need a lot of customized rendering and like the responses to remain at the view level. If you expect your responses to be consistent with the database design go with AMS. If you are a stickler and would like to follow the JSON:API specifications exactly, go with FastJSON-API/JSONAPI-RB
If you wish to see the above examples in action and play around with them, here is a link to the Github repository that I created.
Conclusion
Creating JSON responses in Ruby on Rails might look easy, but with the array of different serializers available, you might be spoilt for choice. The best way to take a decision would be to know who are the potential consumers of your APIs and analyze the expected response. I would also suggest taking a look at their documentation to see which serializer would satisfy all your needs without much overriding.
Everything that we deal with in the inter-connected world today is nothing but "data". Serialization that deals with the translation/storage of this data have become all the more important with the evolution of API-driven applications.
I hope this series of articles dealing with Serialization in Ruby on Rails helps you in choosing the right serialization format and tools for storing and transferring data.
Thank you for reading!
References
- JSON Serialization In Rails - A complete guide
- Which JSON Serializer To Use for a new Rails API
- Wtf is a serializer anyway
- Active Model serializer vs Fast-json-api serializer
- Which JSON serializer to use rails api
- Active Model serializers rails and JSON oh my
- To serialize or not to serialize: activemodel serializers
- jbuilder vs rails-api/active_model_serializers for JSON