Since JavaScript has become the main language of the web and frontend frameworks are based on JavaScript, JSON serialization has become a very important part of many web apps. In this article, I’m going to explain what JSON serialization is, why you may need it in your Rails application and how to leverage existing libraries to implement JSON serialization in a concise and performant way. I will start with a high-level explanation, then discuss some of the most most popular solutions and finally model how to implement JSON serialization using the most optimal solution in a Ruby on Rails application.
Basic Ruby knowledge will be helpful to fully understand the code presented in the tutorial.
JSON (JavaScript Object Notation) is a data format that encodes objects in a string. Such data representation can easily be translated between server and browser but also between server and server. Serialization is a process that transforms an object into that string.
If you’re looking to streamline the initial setup of your Rails projects, consider checking out the ButterCMS Rails starter project. While it’s not directly related to serialization, it provides a solid foundation to get your application up and running faster, allowing you to focus on building out your features with ease.
In Rails, you can use a serializer to define how data from your models should be formatted when sent as a response in JSON. It acts as a blueprint to control what attributes or relationships of a model are included in the output.
This is particularly handy for APIs, where you need to send structured data to clients. For example, instead of exposing the entire model, you can choose specific fields or even modify how certain attributes appear in the response.
To make matters easier for developers, Rails ships with a native serializer known as the Active Model Serializer (gem ‘active_model_serializers’). It’s open-source, well-documented, extensible, and easy to use.
The JSON serialization process consists of two stages: data preparation and transformation to the JSON format.
In the first stage (data preparation), we define how we want our data to be serialized. In the second step (transformation), the actual serialization takes place.
In this stage, we define an attributes hash that lists the fields to include in the serialized output of the object. The keys of this hash must be strings (not symbols), and their corresponding values can be nil initially.
When serialization is triggered, the attributes hash is checked to determine what data to include. The serializer uses instance methods with names matching the keys in the attributes hash to fetch the data. Let’s consider a simple example:
class Person
include ActiveModel::Serialization
attr_accessor :name
attr_accessor :age
def attributes
{ 'name' => nil }
end
end
person = Person.new
person.serializable_hash # => {"name"=>nil}
person.name = "Bob"
person.serializable_hash # => {"name"=>"Bob"}
Here, the `attributes
` hash acts as a blueprint for the data preparation stage.
Here is the full list of best practices that you should stick to when preparing data in order to have good performance:
#to_json
method on a model instance and pass only :option
which contains the array of attributes names you really need: person.to_json(only: [:email, :name])
Ruby on Rails offers several gems to simplify and customize data preparation for your application. While their APIs share some similarities, each gem has distinct features and capabilities. In this guide, I’ll cover some of the most popular data preparation tools and compare them to help you decide on the best solution for your needs.
These tools range from basic to advanced, offering different levels of functionality. For example, Active Model Serialization is the native approach, and it requires you to set up just one gem and is well-documented on the official Rails site.
On the other hand, jsonapi-rb relies on four separate micro-libraries to deliver a wide range of features. Fast JSON API is known for its speed and ease of use, making it a great choice for beginners. If you are looking for the most advanced capabilities, JSONAPI::Serializers is worth exploring. Lastly, if your goal is to serialize attributes directly to a database, Active Record Serialization is the obvious choice.
The following sections will highlight these differences and characteristics in detail, helping you make an informed decision.
If you want to test the code examples below on your side, you can generate a new Rails project and the models by using the following code:
gem install rails
rails new jsontest
cd jsontest
bundle exec rake db:create
bundle exec rails g model post title:string content:text published:boolean
bundle exec rails g model comment author:string body:text post_id:integer
bundle exec rake db:migrate
# app/models/post.rb
class Post < ActiveRecord::Base
has_many :comments
end
# app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :post
end
# Let's load the test data - bundle exec rails c
post = Post.create!(title: "Post", content: "content", published: true)
Comment.create!(post: post, author: "Author", body: "Comment")
ActiveModel::Serialization
implementation is the native serialization method in Rails. It’s also the most popular. Let’s start with adding the gem into our Gemfile:
gem 'active_model_serializers'
Now, here’s how we can update our Post and Comment models to make them serializable:
# app/serializers/post_serializer.rb
class PostSerializer
include ActiveModel::Serializers::JSON
attr_accessor :id, :title, :content, :comments
def attributes
{ 'id' => nil, 'title' => nil, 'content' => nil, 'comments' => nil }
end
def comments
@comments.map { |comment| comment.serializable_hash }
end
end
# app/serializers/comment_serializer.rb
class CommentSerializer
include ActiveModel::Serializers::JSON
attr_accessor :id, :body, :author
def attributes
{ 'id' => nil, 'body' => nil, 'author' => nil }
end
end
With the above changes made, we can test our serializers this way:
# Testing the serializers
comment1 = CommentSerializer.new
comment1.id = 201
comment1.body = "Comment"
comment1.author = "Author"
post = PostSerializer.new
post.id = 12
post.title = "Post"
post.content = "content"
post.comments = [comment1]
post.serializable_hash
# => {"id"=>12, "title"=>"Post", "content"=>"content", "comments"=>[{"id"=>201, "body"=>"Comment", "author"=>"Author"}]}
post.to_json
# => "{\"id\":12,\"title\":\"Post\",\"content\":\"content\",\"comments\":[{\"id\":201,\"body\":\"Comment\",\"author\":\"Author\"}]}"
The jsonapi-rb is an intuitive Ruby library, comprised of 4 independent micro-libraries: jsonapi-parser, jsonapi-renderer, jsonapi-serializable and jsonapi-deserializable.
Let’s add the gem to our Gemfile:
gem 'jsonapi-rails'
Now, we have to update our serializers stored under app/serializers/post_serializer.rb
and app/serializers/comment_serializer.rb
paths:
# app/serializers/post_serializer.rb
class PostSerializer < JSONAPI::Serializable::Resource
type 'posts'
has_many :comments
attributes :id, :title, :content
end
# app/serializers/comment_serializer.rb
class CommentSerializer < JSONAPI::Serializable::Resource
type 'comments'
attributes :id, :author, :body
end
We are ready to test our serializers:
post = Post.joins(:comments).first
renderer = JSONAPI::Serializable::Renderer.new
renderer.render(post, class: { Post: PostSerializer, Comment: CommentSerializer }, include: [:comments]) # => {:data=>{:id=>"113", :type=>:posts, :attributes=>{:id=>113,
:title=>"Post",
:content=>"content"},
:relationships=>{:comments=>{:data=>[{:type=>:comments,
:id=>"2702"}]}}}, :included=>[{:id=>"2702", :type=>:comments,
:attributes=>{:id=>2702, :author=>"Author", :body=>"Comment"}}]}
The lib is a lightning fast JSON:API serializer for Ruby Objects created by Netflix.
Let’s add the gem to our Gemfile:
gem 'fast_jsonapi'
The next step is to update our serializers to use Fast JSON API gem:
# app/serializers/post_serializer.rb
class PostSerializer
include FastJsonapi::ObjectSerializer
attributes :title, :content
has_many :comments
end
# app/serializers/comment_serializer.rb
class CommentSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :body, :author
end
Now, we can prepare our data for serialization:
post = Post.joins(:comments).first
PostSerializer.new(post, include: [:comments]).serializable_hash # =>
{:data=>{:id=>"12", :type=>:post, :attributes=>{:title=>"Post",
:content=>"content"}, :relationships=>{:comments=>{:data=>[{:id=>"201", :type=>:comment}]}}}, :included=>[{:id=>"201", :type=>:comment,
:attributes=>{:id=>201, :body=>"Comment", :author=>"Author"}}]}
RABL (Ruby API Builder Language) is a Ruby templating system for generating JSON.
As usual, the first step is to add the gem in our Gemfile:
gem 'rabl'
This time we will not define serializers under app/serializers
path but instead create the JSON templates. Create new files: app/views/post.rabl
and app/views/comment.rabl
:
# app/views/post.rabl
object @job
attributes :id, :title, :content
child :comments do
extends "comment"
end
# app/views/comment.rabl
object @comment
attributes :id, :author, :body
By default, the gem automatically uses created templates in the Rails controllers when the response in the JSON format is requested. However, we can also prepare data from the console level:
post = Post.joins(:comments).first
Rabl.render(post, 'post', :view_path => 'app/views', :format => :hash) #
=> {:id=>12, :title=>"Post", :content=>"content", :comments=>[{:id=>201, :author=>"Author", :body=>"Comment"}]}
The jsonapi-resourceslibrary is a resource-focused Rails library for developing JSON API compliant servers. It has its own website and documentation - http://jsonapi-resources.com/- which is a big advantage and helps to better understand concepts behind this architecture.
Let’s add the gem to our Gemfile:
gem 'jsonapi-resources'
Now, we have to update existing serializers. Rename PostSerializer to PostResource and put the following code:
class PostResource < JSONAPI::Resource
has_many :comments
attributes :title, :content
end
Do the same with the CommentSerializer (it should be named CommentResource now):
class CommentResource < JSONAPI::Resource
attributes :author, :body
end
and we can generate the data for our JSON response:
post = Post.joins(:comments).first
JSONAPI::ResourceSerializer.new(PostResource, include: ['comments']).serialize_to_hash(PostResource.new(post, nil)) # => {:data=>{"id"=>"1", "type"=>"posts", "links"=>{:self=>"/posts/1"}, "attributes"=>{"title"=>"Title 0", "content"=>"Content 0"}, "relationships"=>{"comments"=>{:links=>{:self=>"/posts/1/relationships/comments", :related=>"/posts/1/comments"}, :data=>[{:type=>"comments", :id=>"25"}]}}}, :included=>[{"id"=>"25", "type"=>"comments", "links"=>{:self=>"/comments/25"}, "attributes"=>{"author"=>"Author 24", "body"=>"Comment 24"}}]}
JSONAPI: Serializers is a simple library for serializing Ruby objects and their relationships.
Add the following line to your Gemfile:
gem 'jsonapi-serializers'
Now, we have to update existing serializers that we created above:
# app/serializers/post_serializer.rb
class PostSerializer
include JSONAPI::Serializer
attribute :id
attribute :title
attribute :content
has_many :comments
end
# app/serializers/comment_serializer.rb
class CommentSerializer
include JSONAPI::Serializer
attribute :id
attribute :author
attribute :body
end
and we can generate the data for our JSON response:
post = Post.joins(:comments).first
JSONAPI::Serializer.serialize(post, include: ['comments']) # =>
{"data"=>{"type"=>"posts", "id"=>"12", "attributes"=>{"id"=>12,
"title"=>"Post", "content"=>"content"}, "links"=>{"self"=>"/posts/12"},
"relationships"=>{"comments"=>{"links"=>{"self"=>"/posts/12/relationships/comments", "related"=>"/posts/12/comments"},
"data"=>[{"type"=>"comments", "id"=>"201"}]}}},
"included"=>[{"type"=>"comments", "id"=>"201",
"attributes"=>{"id"=>201,
"author"=>"Author", "body"=>"Comment"},
"links"=>{"self"=>"/comments/201"}}]}
JBuilder is a gem that provides a simple DSL for declaring JSON structures. This time we don’t have to update our Gemfile because this gem is added by default to Rails. Usually, structures are stored in files with json.jbuilder
extension so we have to create one. Enter the following code in app/views/post2.json.jbuilder
file:
json.id post.id
json.title post.title
json.content post.content
json.comments(post.comments) do |comment|
json.id comment.id
json.author comment.author
json.body comment.body
end
You can now load the template and generate JSON as well as use it without the template:
post = Post.joins(:comments).first
# With template
renderer = ApplicationController.new
renderer.render_to_string('/post2', locals: {post: post}) # => "{\"id\":114,\"title\":\"Title 0\",\"content\":\"Content 0\",\"comments\":[{\"id\":2727,\"author\":\"Author 24\",\"body\":\"Comment 24\"}]}"
# Without template
def jbuild(*args, &block)
Jbuilder.new(*args, &block).attributes!
end
result = jbuild do |json|
json.id post.id
json.title post.title
json.content post.content
json.comments(post.comments) do |comment|
json.id comment.id
json.author comment.author
json.body comment.body
end
end
result # => {"id"=>12, "title"=>"Post", "content"=>"content", "comments"=>[{"id"=>201, "author"=>"Author", "body"=>"Comment"}]}
The ActiveRecord component provides built-in support for serializing attributes directly in the database. This is done using the serialize
method, which can handle formats like JSON, YAML, or even custom serializers. If you want to serialize a class to your database, this is the functionality you should use.
For example, here’s how you could implement custom JSON serialization for a sample class:
class Profile
attr_reader :bio, :website, :social_links
# Deserialize JSON data into a Profile instance
def self.load(payload)
data = payload.present? ? JSON.parse(payload) : {}
new(data["bio"], data["website"], data["social_links"])
end
# Serialize a Profile instance into JSON
def self.dump(profile)
return if profile.nil?
JSON.dump(
{
"bio" => profile.bio,
"website" => profile.website,
"social_links" => profile.social_links
}
)
end
def initialize(bio, website, social_links = {})
@bio = bio
@website = website
@social_links = social_links
end
end
class User < ActiveRecord::Base
# Use the Profile class for custom serialization with JSON
serialize :profile, coder: Profile
end
After saving the user, the profile
column in the database will contain JSON data like this:
{
"bio": "Ruby developer and tech enthusiast.",
"website": "https://janedoe.dev",
"social_links": {
"twitter": "jane_doe",
"github": "janedoe"
}
}
After data preparation, the object is serialized into JSON or other formats. This process converts the hash generated in the preparation stage into a final JSON string. Rails provides methods like to_json
and as_json
for this purpose. Here’s an example of how a typical JSON serialization looks:
class Person
include ActiveModel::Serializers::JSON
attr_accessor :name
attr_accessor :age
def attributes
{ 'name' => nil }
end
end
person = Person.new
person.to_json # => "{\"name\":null}"
person.name = "Bob"
person.to_json # => "{\"name\":\"Bob\"}"
Rails also supports options like :only
, :except
, :methods
, and :include
to customize the serialized output. For example:
person.serializable_hash(only: 'name') # => {"name"=>"Bob"}
person.serializable_hash(include: :address) # => {"name"=>"Bob", "address"=>...}
Next, we will explore the different Ruby gems that can be used to implement serialization, i.e., the transformation of the generated hash to the JSON format.
Oj is a fast JSON parser and Object marshaller as a Ruby gem. Let’s add the new line to our Gemfile in order to install it:
gem 'oj'
Now we can try to transform a hash to the JSON format:
hash = {name: "John Doe", email: "john@doe.com"}
Oj.dump(hash) # => "{\"name\":\"John Doe\",\"email\":\"john@doe.com\"}"
The JSON library is a standard serialization library for Ruby so we can try it directly in our console without adding any extra code:
hash = {name: "John Doe", email: "john@doe.com"}
JSON.generate(hash) # => "{\"name\":\"John Doe\",\"email\":\"john@doe.com\"}"
Yajl is a streaming JSON parsing and encoding library for Ruby.
We have to bundle the gem before we can try it:
gem 'yajl-ruby'
Now we can serialize a sample hash:
require 'yajl'
hash = {name: "John Doe", email: "john@doe.com"}
Yajl::Encoder.encode(hash) # => "{\"name\":\"John Doe\",\"email\":\"john@doe.com\"}"
At this point, we’ve gone through the most popular data preparation and JSON serialization tools. Now, it’s time to choose the most performant pair. I will compare the speed of data preparation and JSON serialization using the code presented in the above paragraphs.
To test, I will create 100 Post
objects and each post will have 25 comments:
100.times do |i|
post = Post.create!(title: "Title #{i}", content: "Content #{i}")
25.times do |i2|
Comment.create!(post: post, author: "Author #{i2}", body: "Comment #{i2}")
end
end
I will use Ruby Benchmark module to compare the data preparation time for each tool.
Below are the solutions ranked fastest to slowest:
The fastest solution is the Fast JSON API and the slowest solutions are JBuilder with RABL.
Fortunately, FAST JSON API is also very intuitive and does not require any extra configuration. All we need to do to start using it is define attributes and associations for given model.
The code used for tests is available here.
The fastest solution is the Oj library. It’s several times faster than its opponents, making it the clear winner.
After our testing, we have our winning couple: Fast JSON API and Oj. It’s time to use both solutions in a Ruby on Rails application to demonstrate how to create performant JSON serialization.
Let’s start by adding required gems to our Gemfile:
gem 'oj'
gem 'fast_jsonapi'
Once you have added the gems, install them by issuing the `bundle install
` command.
Previously, we created a bunch of posts and related comments, so now it’s time to create an endpoint where we provide data in the JSON format. In order to do this, let’s add a new controller class app/controllers/posts_controller.rb
with the following contents:
class PostsController < ApplicationController
def index
posts = Post.joins(:comments)
end
end
To serialize our posts and comments data, we have to define serializers with the attributes we want to serialize. The following code should look familiar since we used it before when demonstrating the usage of the gem:
# app/serializers/post_serializer.rb
class PostSerializer
include FastJsonapi::ObjectSerializer
attributes :title, :content
has_many :comments
end
# app/serializers/comment_serializer.rb
class CommentSerializer
include JSONAPI::Serializer
attribute :id
attribute :author
attribute :body
end
Since the fast_jsonapi gem includes Oj automatically, we don't have to explicitly tell our Rails app to include Oj - https://github.com/Netflix/fast_jsonapi/blob/master/lib/fast_jsonapi/multi_to_json.rb#L57.
We have our serializers prepared so the last thing to do is to render the posts in the JSON format. In order to do this we have to edit our previously created controller:
class PostsController < ApplicationController
def index
posts = Post.joins(:comments)
render json: PostSerializer.new(posts).serialized_json
end
end
and let Rails know that we want to access it by editing the config/routes.rb
file:
resources :posts, only: [:index]
Now you can run rails s
command and access generated data in the JSON format by using http://localhost:3000/posts.json URL.
In this article:
Our solution is extendable and very fast. This approach places serialization logic on a model layer and requires us to directly specify which serializer we want to use. Importantly, you don’t need extra configuration to get started. Using the base code allows you to provide a complete and rich JSON response in your controllers.
Learn how you can use a headless cms with your next Rails app here.