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.

Everything You Need to know about Serialization in Rails: Part I
It was the day we were moving. I was observing how the “Packers and Movers”professionals packed our furniture. For example, the King size bed shown belowhad to be accommodated within a space of about 6-7 inches inside a van. While Ikept wondering how they’d manage this, they dismantled the bed. A…
Serialization in Ruby on Rails for Storage: Part II
Rails framework allows complex objects to be stored in a DB column via the ActiveRecord::Serialization module.This article explains when and how to do it

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
[
    {
        "id": 24,
        "title": "Javascript Training",
        "description": "Master JavaScript With The Most Complete Course On The Market.",
        "created_at": "2021-05-22T17:31:59.856Z",
        "updated_at": "2021-05-22T17:31:59.856Z"
    },
    {
        "id": 25,
        "title": "Ruby on Rails",
        "description": " Learn to make innovative web apps with Ruby on Rails and unleash your creativity",
        "created_at": "2021-05-22T17:31:59.886Z",
        "updated_at": "2021-05-22T17:31:59.886Z"
    }
]
Response for courses#index.json

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
[
    {
        "id": 24,
        "title": "Javascript Training",
        "description": "Master JavaScript With The Most Complete Course On The Market.",
        "created_at": "2021-05-22T17:31:59.856Z",
        "updated_at": "2021-05-22T17:31:59.856Z",
        "topics": [
            {
                "id": 159,
                "name": "Variables and flow control",
                "description": "You will learn the fundamentals of JavaScript",
                "course_id": 24,
                "created_at": "2021-05-22T17:31:59.908Z",
                "updated_at": "2021-05-22T17:31:59.908Z",
                "assignments": [
                    {
                        "id": 157,
                        "description": "Declare a variable called myName and initialize it with a value, on the same line.",
                        "topic_id": 159,
                        "created_at": "2021-05-22T17:31:59.962Z",
                        "updated_at": "2021-05-22T17:31:59.962Z"
                    }
                ]
            }
        ]
    },
    {
        "id": 25,
        "title": "Ruby on Rails",
        "description": " Learn to make innovative web apps with Ruby on Rails and unleash your creativity",
        "created_at": "2021-05-22T17:31:59.886Z",
        "updated_at": "2021-05-22T17:31:59.886Z",
        "topics": [
            {
                "id": 160,
                "name": "Serializers in Rails",
                "description": "You will learn all the API Serializers in Ruby on Rails",
                "course_id": 25,
                "created_at": "2021-05-22T17:31:59.920Z",
                "updated_at": "2021-05-22T17:31:59.920Z",
                "assignments": [
                    {
                        "id": 158,
                        "description": "Render a basic json response from an action controller",
                        "topic_id": 160,
                        "created_at": "2021-05-22T17:31:59.975Z",
                        "updated_at": "2021-05-22T17:31:59.975Z"
                    }
                ]
            }
        ]
    }
]
Response for courses#index.json

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
[
    {
        "id": 24,
        "title": "Javascript Training",
        "description": "Master JavaScript With The Most Complete Course On The Market.",
        "created_at": "2021-05-22T17:31:59.856Z",
        "updated_at": "2021-05-22T17:31:59.856Z",
        "topics": [
            {
                "id": 159,
                "name": "Variables and flow control",
                "description": "You will learn the fundamentals of JavaScript",
                "course_id": 24,
                "created_at": "2021-05-22T17:31:59.908Z",
                "updated_at": "2021-05-22T17:31:59.908Z",
                "is_completed": true,
                "assignments": [
                    {
                        "id": 157,
                        "description": "Declare a variable called myName and initialize it with a value, on the same line.",
                        "topic_id": 159,
                        "created_at": "2021-05-22T17:31:59.962Z",
                        "updated_at": "2021-05-22T17:31:59.962Z"
                    }
                ]
            }
        ]
    },
    {
        "id": 25,
        "title": "Ruby on Rails",
        "description": " Learn to make innovative web apps with Ruby on Rails and unleash your creativity",
        "created_at": "2021-05-22T17:31:59.886Z",
        "updated_at": "2021-05-22T17:31:59.886Z",
        "topics": [
            {
                "id": 160,
                "name": "Serializers in Rails",
                "description": "You will learn all the API Serializers in Ruby on Rails",
                "course_id": 25,
                "created_at": "2021-05-22T17:31:59.920Z",
                "updated_at": "2021-05-22T17:31:59.920Z",
                "is_completed": false
            }
        ]
    }
]
Response for api/v1/courses#index

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
[
    {
        "id": 24,
        "title": "Javascript Training",
        "description": "Master JavaScript With The Most Complete Course On The Market.",
        "topics": [
            {
                "id": 159,
                "name": "Variables and flow control",
                "description": "You will learn the fundamentals of JavaScript",
                "is_completed": true,
                "assignments": [
                    {
                        "id": 157,
                        "description": "Declare a variable called myName and initialize it with a value, on the same line."
                    }
                ]
            }
        ]
    },
    {
        "id": 25,
        "title": "Ruby on Rails",
        "description": " Learn to make innovative web apps with Ruby on Rails and unleash your creativity",
        "topics": [
            {
                "id": 160,
                "name": "Serializers in Rails",
                "description": "You will learn all the API Serializers in Ruby on Rails",
                "is_completed": false
            }
        ]
    }
]
Response for api/v1/courses#index

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:

[
    {
        "id": 24,
        "title": "Javascript Training",
        "description": "Master JavaScript With The Most Complete Course On The Market.",
        "topics": [
            {
                "id": 159,
                "name": "Variables and flow control",
                "description": "You will learn the fundamentals of JavaScript",
                "is_completed": true,
                "assignments": [
                    {
                        "id": 157,
                        "description": "Declare a variable called myName and initialize it with a value, on the same line."
                    }
                ]
            }
        ]
    },
    {
        "id": 25,
        "title": "Ruby on Rails",
        "description": " Learn to make innovative web apps with Ruby on Rails and unleash your creativity",
        "topics": [
            {
                "id": 160,
                "name": "Serializers in Rails",
                "description": "You will learn all the API Serializers in Ruby on Rails",
                "is_completed": false
            }
        ]
    }
]
Response for courses#index.json

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
{
    "data": [
        {
            "id": "24",
            "type": "course",
            "attributes": {
                "id": 24,
                "title": "Javascript Training",
                "description": "Master JavaScript With The Most Complete Course On The Market."
            },
            "relationships": {
                "topics": {
                    "data": [
                        {
                            "id": "159",
                            "type": "topic"
                        }
                    ]
                }
            }
        },
        {
            "id": "25",
            "type": "course",
            "attributes": {
                "id": 25,
                "title": "Ruby on Rails",
                "description": " Learn to make innovative web apps with Ruby on Rails and unleash your creativity"
            },
            "relationships": {
                "topics": {
                    "data": [
                        {
                            "id": "160",
                            "type": "topic"
                        }
                    ]
                }
            }
        }
    ],
    "included": [
        {
            "id": "157",
            "type": "assignment",
            "attributes": {
                "id": 157,
                "description": "Declare a variable called myName and initialize it with a value, on the same line."
            }
        },
        {
            "id": "159",
            "type": "topic",
            "attributes": {
                "id": 159,
                "name": "Variables and flow control",
                "description": "You will learn the fundamentals of JavaScript",
                "is_completed": true
            },
            "relationships": {
                "assignments": {
                    "data": [
                        {
                            "id": "157",
                            "type": "assignment"
                        }
                    ]
                }
            }
        },
        {
            "id": "160",
            "type": "topic",
            "attributes": {
                "id": 160,
                "name": "Serializers in Rails",
                "description": "You will learn all the API Serializers in Ruby on Rails",
                "is_completed": false
            },
            "relationships": {}
        }
    ]
}
Response for courses#index.json

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
{
    "data": [
        {
            "id": "24",
            "type": "courses",
            "attributes": {
                "title": "Javascript Training",
                "description": "Master JavaScript With The Most Complete Course On The Market."
            },
            "relationships": {
                "topics": {
                    "data": [
                        {
                            "type": "topics",
                            "id": "159"
                        }
                    ]
                }
            }
        },
        {
            "id": "25",
            "type": "courses",
            "attributes": {
                "title": "Ruby on Rails",
                "description": " Learn to make innovative web apps with Ruby on Rails and unleash your creativity"
            },
            "relationships": {
                "topics": {
                    "data": [
                        {
                            "type": "topics",
                            "id": "160"
                        }
                    ]
                }
            }
        }
    ],
    "included": [
        {
            "id": "159",
            "type": "topics",
            "attributes": {
                "name": "Variables and flow control",
                "description": "You will learn the fundamentals of JavaScript",
                "is_completed": true
            },
            "relationships": {
                "course": {
                    "meta": {
                        "included": false
                    }
                },
                "assignments": {
                    "data": [
                        {
                            "type": "assignments",
                            "id": "157"
                        }
                    ]
                }
            }
        },
        {
            "id": "160",
            "type": "topics",
            "attributes": {
                "name": "Serializers in Rails",
                "description": "You will learn all the API Serializers in Ruby on Rails",
                "is_completed": false
            },
            "relationships": {
                "course": {
                    "meta": {
                        "included": false
                    }
                }
            }
        },
        {
            "id": "157",
            "type": "assignments",
            "attributes": {
                "description": "Declare a variable called myName and initialize it with a value, on the same line."
            },
            "relationships": {
                "topic": {
                    "meta": {
                        "included": false
                    }
                }
            }
        },
        {
            "id": "158",
            "type": "assignments",
            "attributes": {
                "description": "Render a basic json response from an action controller"
            },
            "relationships": {
                "topic": {
                    "meta": {
                        "included": false
                    }
                }
            }
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}
Response for courses#index.json

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

  1. JSON Serialization In Rails - A complete guide
  2. Which JSON Serializer To Use for a new Rails API
  3. Wtf is a serializer anyway
  4. Active Model serializer vs Fast-json-api serializer
  5. Which JSON serializer to use rails api
  6. Active Model serializers rails and JSON oh my
  7. To serialize or not to serialize: activemodel serializers
  8. jbuilder vs rails-api/active_model_serializers for JSON