When working with Rails, it is simple to create a JSON object. Using an index
or show
method, will provide a list of all the data available.
#The Rails index method
def index
render json: Book.all
end
#Returns this JSON hash
[
{
"id": 1
"title": "Pride and Prejudice",
"author": "Jane Austen",
"description": "Set in England in the early 19th century, Pride and Prejudice tells the story of Mr and Mrs Bennet's five unmarried daughters after the rich and eligible Mr Bingley and his status-conscious friend, Mr Darcy, have moved into their neighbourhood. While Bingley takes an immediate liking to the eldest Bennet daughter, Jane, Darcy has difficulty adapting to local society and repeatedly clashes with the second-eldest Bennet daughter, Elizabeth.",
"image": "https://d1w7fb2mkkr3kw.cloudfront.net/assets/images/book/lrg/9781/4351/9781435159631.jpg",
"year": 1813,
"genre": "Romance",
"isbn": 9780140430721,
"publisher": "T. Egerton, Whitehall",
"length": 259,
"reading_time": 1,
"rating": 5,
"created_at": "2022-04-14T15:42:23.682Z",
"updated_at": "2022-04-14T15:42:23.682Z"
}
]
That is a lot of information! What happens in the scenario where I only need the title, author, and rating? I can always pass all the information and only use what I need. That could work! While continuing to work another scenario arises where only the Title, Image, and Rating are needed. Again, I can pass the whole object and only use the data required. But that is a lot of unnecessary information. Luckily, there is a way in Rails to pass only the information you need. Using .to_json will allow you to pass only certain information.
#Adding to_json to the show method
def show
books = Book.find(params[:id])
render json: book.to_json(only:[:id, :title, :image, :rating])
end
#Will return the following:
[
{
"id": 1,
"title": "Pride and Prejudice",
"image": "https://d1w7fb2mkkr3kw.cloudfront.net/assets/images/book/lrg/9781/4351/9781435159631.jpg",
"rating": 5
}
]
That is much more efficient! But this method can quickly become overwhelming and crowded. If we want a method where we can observe good coding practice and have separation of concerns, we'll have to look at something different.
Serializers
Active Model Serializers (AMS) are a gem you can add to your project to customize how your JSON is rendered. It is a way to get the exact data you need while keeping separation of concerns.
To start, you need to install the gem.
gem 'active_model_serializers'
You can do this to on an active Rails project you are working on, just run
bundle install
after adding the gem, or add it during the setup portion, with all your gems.
After installing, you can generate the serializers using Rails generators.
rails g serializer book
This will generate a serializers folder in the app folder of your project. In the folder there will be a file called book_serializer.rb.
class BookSerializer < ActiveModel::Serializer
attributes :id
end
You can add as many attributes as needed. For the example above where we only needed the id, title, image, and rating, we could have the following:
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :image, :rating
end
#Will return the following:
[
{
"id": 1,
"title": "Pride and Prejudice",
"image": "https://d1w7fb2mkkr3kw.cloudfront.net/assets/images/book/lrg/9781/4351/9781435159631.jpg",
"rating": 5
}
]
Fantastic! We got exactly what we needed, and good coding practices are observed with separation of concerns.
Once again, all the steps.
- Install Active Model Serializers gem
- Create a Serializer
- Add required attributes
Custom Methods
We can also create custom methods for our serializers.
class BookSummarySerializer < ActiveModel::Serializer
attributes :id, :summary, :author
def summary
"#{self.object.title} - #{self.object.description[0..49]}..."
end
end
We create a method called summary and define what it does. In this case we are interpolating the title and the first 49 characters of the book description. After creating the method, we just add it to the list of attributes, and it will show up in the JSON.
#Will return the following:
[
{
"id": 1,
"summary": "Pride and Prejudice - Set in England in the early 19th century, Pride an...",
"author": "Jane Austen"
}
]
Let's go over those steps again.
- Create a custom method in your serializer
- Add the new method to your attributes
You can add as many custom methods as needed, but after a certain point, we can run into the issue of separation of concerns again. To separate our concerns, we can create a custom serializer.
Custom Serializers
To create a custom serializer for the method shown above, we need to start by creating the serializer. We can create a new file physically in our serializers folder, or we can use a generator again.
If you add a file by hand, make sure to observe proper syntax. Serializers need to be singular and named using snake_case. You will also need to add the class using PascalCase, and shovel it to your ActiveModel.
class BookSummarySerializer < ActiveModel::Serializer
After creating or generating the new serializer called BookSummarySerializer, we need to create the custom method, and add it to the attributes.
class BookSummarySerializer < ActiveModel::Serializer
attributes :id, :summary, :author
end
def summary
"#{self.object.title} - #{self.object.description[0..49]}..."
end
#Will return the following:
[
{
"id": 1,
"summary": "Pride and Prejudice - Set in England in the early 19th century, Pride an...",
"author": "Jane Austen"
}
]
After completing the custom serializer, it needs to be added to your controller. Rails will automatically check for a serializer with the same name as your controller, so if you want your custom serializer to show, you need to add it to your controller after rendering to JSON.
class BooksController < ApplicationController
def index
render json: Book.all
end
def show
book = Book.find(params[:id])
render json: book, serializer: BookSummarySerializer
end
end
Steps to create a custom serializer:
- Create a new serializer file with the custom name
- Add all the needed information to your custom serializer
- Add the custom serializer to the proper controller
Associated Data
If your data has associations created through models, you can access that data in your serializers as well. Building on our Book example, we can have an association where a book can have many reviews.
Book-<Reviews
You can show associated reviews of your books show in your JSON through serializers by simply adding the association.
If you have a many through relationship, you only need to write has_many for the info to show up.
Ex: Author -< Books >- Genre
An author has many books and has many genres through books. If you wanted to have the associated genres to your author on your JSON, you add thehas_many :genres
macro without needing to specify the "through" relationship.
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :author
has_many :reviews
end
#Will return the following:
[
{
"id": 1,
"title": "Pride and Prejudice",
"author": "Jane Austen"
"reviews": [
{
"name": "Bob"
"review": "Awesome classic!"
},
{
"name": "Nancy"
"review": "Best book ever!"
},
{
"name": "Lea"
"review": "Meh.. Not my speed."
}
]
}
]
Great! Now we have our book and all its related reviews!
Let's go over those steps again.
- Have associations created in your models
- Add required associations to your serializer
Deeply Nested Data
Let's look at a different example where we have a different relationship.
Author-< Book -< Review
The author has many books which have many reviews. If we wanted to have that information appear in our JSON, could we do that through serializers as well? Active Model Serializers only nest one level deep to protect from overcomplicating our data. If we still wanted the deeply nested data, we could overwrite AMS behavior.
We'll start by looking at our serializers.
#Author Serializer
class AuthourSerializer < ActiveModel::Serializer
attributes :id, :first_name, :last_name
has_many :books
end
#Book Serializer
class BookSerializer < ActiveModel::Serializer
attributes :id, :title
belongs_to :author
has_many :reviews
end
#Review Serializer
class ReviewSerializer < ActiveModel::Serializer
attributes :id, :name, :review
end
As we can see above, the author serializer includes its association to books, and the book serializer includes its association to author and reviews. For our deeply nested models to work, we need to include the associations in the proper serializers.
After creating the proper associations, we need to add information to our author controller so we can override the AMS behavior.
class AuthorController < ApplicationController
def index
render json: Book.all
end
def show
author = Author.find(params[:id])
render json: author, include: ['books', 'books.reviews']
end
end
In the show method, after the render to JSON, there is code that tells Active Model Serializer that we want to render information for the author, and to also include information for the books associated with that author, and for the reviews associated with those books. Now our JSON will return the following:
[
{
"id": 1,
"first_name": "Jane",
"last_name": "Austen",
"books": [
{
"id": 1,
"title": "Pride and Prejudice",
"reviews": [
{
"name": "Bob"
"review": "Awesome classic!"
},
{
"name": "Nancy"
"review": "Best book ever!"
},
{
"name": "Lea"
"review": "Meh.. Not my speed."
}
]
}
]
}
]
Mission accomplished!
Let's go over the steps one more time.
- Create serializers
- Add the associations to the proper serializers
- Include the information we want to render in the associated controller
Final Thoughts
Serializers are very powerful and useful. We can do so much with them and get our JSON exactly how we need it to be. One thing we should keep in mind, however, is that with each of the serializers, custom serializers, and custom methods we are adding complexity to our program. All the macros and methods start to add up quickly and can make things very complex.
Sources
Cover Image
Flatiron
Serializer Docs
Top comments (1)
you forgot to do for Book.all