Using Ruby on Rails and ActiveModel::Serializer (AMS) to Control Your Data (Review)
ericksong91
Posted on May 25, 2023
Introduction
Hi y'all,
This is a quick review on how to utilize the Serializer gem for Ruby on Rails. ActiveModel::Serializer
or AMS, is a great way to control how much information an API sends as well as a way to include nested data from associations.
There are many instances where your backend will store extraneous data that isn't needed for the frontend or data that should not be shown. For example, for a table of User data, you would not want to display password hashes. Or if you have a relational database about Books with many Reviews, you would not want to include all the Reviews with every Books fetch request until you explicitly needed it.
This tutorial will assume you already know the basics of using Rails models and migrations along with making routes and using controllers.
Setting Up Our Relational Database
Before we begin, lets come up with a simple relational table with a many-to-many relationship:
Museums have many Users (Artists) through Paintings
Users (Artists) have many Museums through Paintings
The table shows a many-to-many relationship between Museums
and Users
(Artists), joined by Paintings
. The arrows in the table indicate the foreign keys associated with each table in Paintings
.
Now lets make our models and migrations:
# models/museums.rb
class Museum < ApplicationRecord
has_many :paintings
has_many :users, -> { distinct }, through: :paintings
end
# models/paintings.rb
class Painting < ApplicationRecord
belongs_to :user
belongs_to :museum
end
# models/users.rb
class User < ApplicationRecord
has_many :paintings, dependent: :destroy
has_many :museums, -> { distinct }, through: :paintings
end
*Note: -> { distinct } will make sure there are no duplicate data when the User
or Museum
data is retrieved.
The schema after migration should look something like this:
# db/schema.rb
create_table "museums", force: :cascade do |t|
t.string "name"
t.string "location"
t.string "bio"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "paintings", force: :cascade do |t|
t.string "name"
t.string "bio"
t.string "img_url"
t.integer "year"
t.integer "user_id"
t.integer "museum_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "username"
t.string "password_digest"
t.string "bio"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
In your seeds.rb
file, make some generic seed data.
# db/seeds.rb
# Example Seed Data:
user = User.create!(username: "John", password:"asdf",
password_confirmation: "asdf", bio: "Test")
musuem = Museum.create!(name: "Museum of Cool Things", location:
"Boston", bio: "Cool Test Museum")
painting = Painting.create!(name: "Mona Lisa", bio: "very famous",
img_url: "url of image", year: 1999,
user_id: 1, museum_id: 1
Now in our Config folder, lets make a simple Index route just for Users
in routes.rb
.
# config/routes.rb
Rails.application.routes.draw do
resources :users, only: [:index]
end
Then in our users_controller.rb
, lets make a simple function that shows all Users.
# controllers/users_controller.rb
class UsersController < ApplicationController
def index
user = User.all
render json: users
end
end
Now boot up your server and go to /users
to see what kind of data you're getting back:
[{
"id":1,
"username":"John",
"password_digest":"$2a$12$GXCFijd75p4VXj3OazNpFu52.nKbd0ETBbZUutVZAQqlyGCVphPGW",
"bio":"Test.",
"created_at":"2023-05-14T01:47:42.292Z",
"updated_at":"2023-05-14T01:47:42.292Z"
},
Yikes! You definitely don't want to have all this information, especially not the password hash. Lets get into our serializers and fix that.
Setting Up and Installing AMS
First, we want to install the gem:
gem "active_model_serializers"
Once the gem has been installed, you can start to generate your serializer files using rails g serializer name_of_serializer
in your console.
Lets add one for each of our models:
rails g serializer museum
rails g serializer user
rails g serializer painting
This should make a Serializer folder in your project directory with the files museum_serializer.rb
, user_serializer.rb
and painting_serializer.rb
. Once we have these files, we can now control what kind of information we're getting.
It is important to note that as long as you're following naming conventions, Rails will implicitly look for the serializer that matches the model class name.
For example, for Users:
# models/user.rb
class User < ApplicationRecord
# code
end
# serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
end
Rails will look for the serializer that has the class name of User
then the word 'Serializer' (UserSerializer
). It will look for this naming convention for the default serializer.
Now lets try modifying some of the information we get back.
Managing Data from Fetch Requests
Excluding Information
Lets reload our /users
GET
request and see what happens now.
[{}]
Looks like we're getting no information now but don't fear; we just need to tell the serializer what data we want. Lets start with just grabbing :id
, :username
and their :bio
by adding some attributes
.
# serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :username, :bio
end
Reloading our GET
request, we now have:
[{
"id":1,
"username":"John",
"bio":"Test"
}
Perfect! Now we can control what information we want to get from our GET
request.
Adding Nested Data
Now lets say we want to include the paintings that belong to a user. We already have our relationships mapped out in our model files but we also need to add these macros to our serializers.
# serializers/painting_serializer.rb
class PaintingSerializer < ActiveModel::Serializer
attributes :id, :name, :bio, :img_url, :user_id, :museum_id,
:year
belongs_to :user
belongs_to :museum
end
# serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :username
has_many :paintings
end
Refreshing /users
again will display:
[{
"id":2,
"username":"John",
"bio":"Test",
"paintings":[
{
"id":1,
"name":"Mona Lisa",
"bio":"very famous",
"img_url":"url of image",
"user_id":1,
"museum_id":1,
"year":1999
}]}
Throwing this relationship into the UserSerializer
and PaintingSerializer
will allow you receive nested data of Paintings belonging to a User. The information nested in the User data will reflect what is in the PaintingSerializer
!
Adding a Different Serializer for the Same Model
You can also add a new serializer that includes different information from the default serializer. Lets say for a single User
, we want them to have a list of Museums
and Paintings
on their profile page.
Make a new serializer called user_profile_serializer.rb
. We'll use this serializer to also include the museum data for a user.
# serializers/museum_serializer.rb
class MuseumSerializer < ActiveModel::Serializer
attributes :id, :name, :bio, :location
end
# serializers/painting_serializer.rb
class PaintingSerializer < ActiveModel::Serializer
attributes :id, :name, :bio, :img_url, :user_id, :museum_id,
:year
belongs_to :user
belongs_to :museum
end
# serializers/user_profile_serializer.rb
class UserProfileSerializer < ActiveModel::Serializer
attributes :id, :username, :bio
has_many :paintings
has_many :museums, through: :paintings
end
Rails will not use this serializer file by default but you can call for it when rendering your JSON. In your users_controller.rb
file, you can change what serializer you use for a specific request. Make the appropriate changes in routes.rb
by including :show
and adding it to the controller.
# config/routes.rb
Rails.application.routes.draw do
resources :users, only: [:index, :show]
end
# controllers/users_controller.rb
# For this example, we'll just assume we're looking for the User of ID 1
class UsersController < ApplicationController
def index
users = User.all
render json: users
end
def show
user = User.find_by(id: 1)
render json: user, status: :created,
serializer: UserProfileSerializer
end
end
Loading up /users/1
gives us:
[{
"id":2,
"username":"John",
"bio":"Test",
"paintings":[
{
"id":1,
"name":"Mona Lisa",
"bio":"very famous",
"img_url":"url of image",
"user_id":1,
"museum_id":1,
"year":1999
}],
"museums": [
{
"id": 1,
"name": "Museum of Cool Things",
"bio": "Cool Test Museum",
"location": "Boston",
}]}
We look for the specific User, and if that User is found, we render their information and include the museums nested data as well.
Conclusion
Serializers are very powerful as they let you control the data your API is sending.
Notes
Please let me know in the comments if I've made any errors or if you have any questions. Still new to Ruby on Rails and AMS but would like to know your thoughts :)
Extra Reading, Credits and Resources
A Quick Intro to Rails Serializers
Posted on May 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.