DEV Community

ericksong91
ericksong91

Posted on • Updated on

Using Ruby on Rails and ActiveModel::Serializer (AMS) to Control Your Data (Review)

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, Users (Artists) and Paintings

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
Enter fullscreen mode Exit fullscreen mode

*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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
},
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

[{}]
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Reloading our GET request, we now have:

[{
"id":1,
"username":"John",
"bio":"Test"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}]}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
}]}
Enter fullscreen mode Exit fullscreen mode

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

ActiveModel::Serializer Repo

A Quick Intro to Rails Serializers

Quickstart Guide to Using Serializer

DB Diagram

Top comments (0)