Cerealizing with Serializers

If you are reading this, you might, like me, be relatively new to Ruby on Rails, and are still getting familiar with some of the features it has to offer. Personally, when I first heard about serializers, they seemed a bit redundant. We are already han…


This content originally appeared on DEV Community and was authored by michael-neis

If you are reading this, you might, like me, be relatively new to Ruby on Rails, and are still getting familiar with some of the features it has to offer. Personally, when I first heard about serializers, they seemed a bit redundant. We are already handling what we are rendering in our controller, so why add an extra layer? While it may seem like extra work in a small application, as you expand your database, you will start to realize that sorting through your data in the controllers becomes increasingly difficult.

For now, let's just take a look at a smaller application to understand the basics. Let's say we've built out the basics for a database for cereal companies, and we want to show those cereals to users buying them online (for now, we won't worry about creating users, we will just focus on the cereal side). Right now, we aren't using serializers, we just have a controller and a model for Company, CEO, Charity, and of course, Cereal. For our purposes, a Company has_many CEOs, Charities and Cereals.

Image description

We just have some simple models for now:

class Company < ApplicationRecord
  has_many :ceos
  has_many :charities
  has_many :cereals
end

class Ceo < ApplicationRecord
  belongs_to :company
end

class Charity < ApplicationRecord
  belongs_to :company
end

class Cereal < ApplicationRecord
  belongs_to :company
end

Alright, now let's take a look at our routes.rb.

Rails.application.routes.draw do
  resources :cereals
  resources :companies
  resources :charities
  resources :ceos
end

Using resources will automatically include all RESTful routes, which means we can define index and show in our cereal controller.

class CerealsController < ApplicationController

    def index
        cereals = Cereal.all
        render json: cereals
    end

    def show
        cereal = Cereal.find(params[:id])
        render json: cereal
    end

end

Note: using a .find in the show route on its own is not best practice, because if an incorrect id is provided, there will be no rescue from the error. If you would like to know more about handling errors, check out the Rails documentation for info on error handling and validations. Since we are only focused on setting up serializers, we'll skip this for now.

Ok, so as it stands right now, let's take a look at what returns to us on a GET request to '/cereals'.

[
    {
        "id": 1,
        "name": "Cheerios",
        "company_id": 1,
        "price": 3.49,
        "ingredients": "*list of ingredients*",
        "time_to_make": 1.3,
        "transportation_schedule": "*list of schedule details*",
        "expiration_date": "Jan 12, 2022",
        "warehouse_location": "Cleveland",
        "created_at": "2021-12-17T17:55:35.618Z",
        "updated_at": "2021-12-17T17:55:35.618Z"
    },
    {
        "id": 2,
        "name": "Chex",
        "company_id": 1,
        "price": 3.99,
        "ingredients": "*list of ingredients*",
        "time_to_make": 0.9,
        "transportation_schedule": "*list of schedule details*",
        "expiration_date": "Jan 10, 2022",
        "warehouse_location": "Des Moines",
        "created_at": "2021-12-17T17:55:35.633Z",
        "updated_at": "2021-12-17T17:55:35.633Z"
    },
    {
        "id": 3,
        "name": "Lucky Charms",
        "company_id": 1,
        "price": 4.49,
        "ingredients": "*list of ingredients*",
        "time_to_make": 1.5,
        "transportation_schedule": "*list of schedule details*",
        "expiration_date": "Jan 02, 2022",
        "warehouse_location": "Minneapolis",
        "created_at": "2021-12-17T17:55:35.648Z",
        "updated_at": "2021-12-17T17:55:35.648Z"
    }
]

It works! But that is a lot of data that we don't necessarily need. If the request is being made by a user that is hoping to buy a box of cereal, there is definitely some data that can be left out. When you are buying cereal, you don't need to know the id of that particular box of cereal, how long it took to make, the transportation schedule, the warehouse it is stored in, or when it was added or updated to the database. What we could do is specify what parts of the hash we want rendered in our controller, but we would have to repeat that process on any and every request we make as we continue to build out the application. What we can do instead is use serializers. To get started, we have to install the serializers gem.

bundle add active_model_serializers

This will add the gem to your gemfile and install it at the same time. Now we can create a serializer for cereal with rails g serializer cereal. You will see that this automatically create a serializers folder in your application and add a cereal_serializer to it. I know what you may be thinking, "Generating serializers for all of my models sounds exhausting and time consuming!" And if you do it the way we just did, you'd be right. But, a bonus to the serializer gem is that if you install it at the beginning of your application, before creating any of your migrations, models, or controllers, running rails g cereal will also create a serializer so that you don't have to. By default, it will even include any relationships and attributes that you added to your generation. Since we didn't do that this time around, this is what our serializer looks like right now:

class CerealSerializer < ActiveModel::Serializer
  attributes :id
end

Because it only includes the attribute :id, the only thing that will be rendered it the cereal id. Taking a look at our GET request again, we see:

[
    {
        "id": 1
    },
    {
        "id": 2
    },
    {
        "id": 3
    }
]

Pretty cool, but still not the information we want to show our buyers. Looking at all of the data from the previous request, the realistic attributes we might want to include are name, company_id, price, ingredients, and expiration date. So let's do that.

class CerealSerializer < ActiveModel::Serializer
  attributes :name, :company_id, :price, :ingredients,
:expiration_date
end

And see the new return results:

[
    {
        "name": "Cheerios",
        "company_id": 1,
        "price": 3.49,
        "ingredients": "*list of ingredients*",
        "expiration_date": "Jan 12, 2022"
    },
    {
        "name": "Chex",
        "company_id": 1,
        "price": 3.99,
        "ingredients": "*list of ingredients*",
        "expiration_date": "Jan 10, 2022"
    },
    {
        "name": "Lucky Charms",
        "company_id": 1,
        "price": 4.49,
        "ingredients": "*list of ingredients*",
        "expiration_date": "Jan 02, 2022"
    }
]

Sweet! Now, without changing anything in our controller, let's look at the results of a Get request to a specific cereal id, triggering the show route.

{
    "name": "Cheerios",
    "company_id": 1,
    "price": 3.49,
    "ingredients": "*list of ingredients*",
    "expiration_date": "Jan 12, 2022"
}

As you can see, the specified attributes of the cereal serializer are being used in both routes from the cereal controller. This way, we can specify what attributes we want in a scope that will affect all of our routes.

But what about that company_id that we are getting? That won't be very useful to a buyer. We want to be able to see the actual company name, not just the id. Serializers have the ability to show associations through has_many and belongs_to, just like in our models. You may notice that if you generate a serializer using resource, a model that belongs to another will have a serializer with a has_one association instead of belongs_to. These operate the same, but I find it easier and more consistent to use belongs_to. Let's update our cereal serializer with a belongs_to and see what we get. We also remove the company_id attribute, since we won't be needing that.

class CerealSerializer < ActiveModel::Serializer
  attributes :name, :company_id, :price, :ingredients, :expiration_date
  belongs_to :company
end

On a show route, this will return:

{
    "name": "Cheerios",
    "price": 3.49,
    "ingredients": "*list of ingredients*",
    "expiration_date": "Jan 12, 2022",
    "company": {
        "id": 1,
        "name": "General Mills",
        "employees": 40000,
        "healthcare_plan": "*plan description*",
        "revenue": 17000000000.0,
        "created_at": "2021-12-17T19:05:02.950Z",
        "updated_at": "2021-12-17T19:05:02.950Z"
    }
}

Great! Now we have the company details from that association. But again, we're getting information that we don't need. There's a few ways we could handle this now.

The first way we can handle it is to create serializers for our other classes, and use them through our belongs_to association. Let's create a serializer for company with rails g serializer company, and edit the attributes it shows.

class CompanySerializer < ActiveModel::Serializer
  attributes :name
end

Here's the result of requesting now:

{
    "name": "Cheerios",
    "price": 3.49,
    "ingredients": "*list of ingredients*",
    "expiration_date": "Jan 12, 2022",
    "company": {
        "name": "General Mills"
    }
}

Sweet! We now have the company name associated with our cereal. There are a few problems with this solution, however. For one, as you can see, in order to access that company name, we have to read down two levels to the nested data. Not too ideal. Another is that this serializer will be filtering all of the company requests as well. So, a GET request to '/companies' will only give us:

[
    {
        "name": "General Mills"
    },
    {
        "name": "Kellogg's"
    },
    {
        "name": "Quaker Oats"
    },
]

If all we are using this database for is to show the company names, this is fine, but we may want to be able to access different attributes of those parent models.

The second solution fixes those problems. We can create custom serializer methods and include them in the attributes. First we'll want to remove the belongs_to :company from our cereal serializer.

class CerealSerializer < ActiveModel::Serializer
  attributes :name, :price, :ingredients, :expiration_date, 
:company_name


  def company_name
    object.company.name
  end
end

We can define methods within our serializers to perform data handling, then all we need to do is add that method name to our attributes list. object is a moniker for the object that is being passed to the serializer, similar to self in class models. Here's what we get on this request:

{
    "name": "Cheerios",
    "price": 3.49,
    "ingredients": "*list of ingredients*",
    "expiration_date": "Jan 12, 2022",
    "company_name": "General Mills"
}

Much cleaner! Using custom methods is a pretty slick way to include associated data when we only want certain attributes of that data. Now, as you can see, the company name is on the same level as all of our other data, so no need to dig into nested data.

To show another example of custom methods, I am now going to use the CEO and Charity models that were defined earlier, but never used. Let's say a company wanted to include some details of a charitable cause they are backing and a motto from their fearless CEO on the box they are selling. Obviously, in real life, these would just be mass-printed on the boxes during packing, but since we are working in a digital world, we need to find a way to render that information. Since both are children of Company, I will just be using Ceo.first and Charity.first in reference to each instance I want to use.

class CerealSerializer < ActiveModel::Serializer
attributes :name, :price, :ingredients, :expiration_date,
:company_name, :ceo_name, :ceo_motto, :charity_name,
:charity_cause


  def company_name
    object.company.name
  end

  def ceo_name
    object.company.ceos.first.name
  end

  def ceo_motto
    object.company.ceos.first.motto
  end

  def charity_name
    object.company.charities.first.name
  end

  def charity_cause
    object.company.charities.first.cause
  end

end

And here's our render:

{
    "name": "Cheerios",
    "price": 3.49,
    "ingredients": "*list of ingredients*",
    "expiration_date": "Jan 12, 2022",
    "company_name": "General Mills",
    "ceo_name": "Jeff Harmening",
    "ceo_motto": "Cereal is our passion.",
    "charity_name": "General Mills Foundation",
    "charity_cause": "Advancing regenerative agriculture"
}

The way in which you go about receiving data will differ based on how your database is set up and nested, but hopefully this gives you an idea of how to start that process.


This content originally appeared on DEV Community and was authored by michael-neis


Print Share Comment Cite Upload Translate Updates
APA

michael-neis | Sciencx (2021-12-17T23:24:32+00:00) Cerealizing with Serializers. Retrieved from https://www.scien.cx/2021/12/17/cerealizing-with-serializers/

MLA
" » Cerealizing with Serializers." michael-neis | Sciencx - Friday December 17, 2021, https://www.scien.cx/2021/12/17/cerealizing-with-serializers/
HARVARD
michael-neis | Sciencx Friday December 17, 2021 » Cerealizing with Serializers., viewed ,<https://www.scien.cx/2021/12/17/cerealizing-with-serializers/>
VANCOUVER
michael-neis | Sciencx - » Cerealizing with Serializers. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/12/17/cerealizing-with-serializers/
CHICAGO
" » Cerealizing with Serializers." michael-neis | Sciencx - Accessed . https://www.scien.cx/2021/12/17/cerealizing-with-serializers/
IEEE
" » Cerealizing with Serializers." michael-neis | Sciencx [Online]. Available: https://www.scien.cx/2021/12/17/cerealizing-with-serializers/. [Accessed: ]
rf:citation
» Cerealizing with Serializers | michael-neis | Sciencx | https://www.scien.cx/2021/12/17/cerealizing-with-serializers/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.