Using Ruby on Rails and ActiveModel::Serializer (AMS) to Control Your Data (Review)
Tags: ruby, rails, webdev, beginners
Published: 2023-05-25 23:33:46

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5eevnwrksv7qlmdjytx.png) 

`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

[ActiveModel::Serializer Repo](https://github.com/rails-api/active_model_serializers/tree/v0.10.6)

[A Quick Intro to Rails Serializers](https://medium.com/@maxfpowell/a-quick-intro-to-rails-serializers-b390ced1fce7)

[Quickstart Guide to Using Serializer](https://itnext.io/a-quickstart-guide-to-using-serializer-with-your-ruby-on-rails-api-d5052dea52c5)

[DB Diagram](https://dbdiagram.io/home)