Donte Ladatto
Posted on September 23, 2022
Today I thought I'd run through a few of the powerful things that the ActiveModel::Serializer gem brings to the table when used in a Ruby on Rails application. Sure, there are faster alternatives, but AMS is the first one I learned how to use, and thus it holds a special place in my heart. Quick question before we dive too much deeper, though...
Why even use serializers?
If you've ever constructed an API using Rails, chances are you at least have a surface level understanding of what serializers are. You may think them unnecessary; why bother creating a whole new file when you can just dictate the shape of your response right inside of your controller action? For example, you have an index action and all you need it to do is send back a JSON response with all of the objects and their attributes minus the timestamps:
class ObjectsController < ApplicationController
def index
objects = Object.all
render json: objects, except: [:created_at, :updated_at]
end
end
This works just fine, and I'll grant that it does seem a little extra to deploy a serializer for such a small task. But consider the model-view-controller (or MVC) architecture, in which the controller's purpose is to take the client request and communicate with the model to deliver a response to the view layer. With separation of concerns in mind, determining how to display the response data to the user would seem to fall more under the view umbrella, no? So let's add AMS to our gemfile and play around with some serializers!
Creating a serializer with AMS
When you add ActiveModel::Serializer to your Rails application, the rails generate resource
command will automatically create a serializer for the new model. You can also generate a serializer for an existing model, but it's important to follow the correct naming conventions. Unlike controller names, serializers are named after the singular form of the model. If we want to generate a serializer for our Object model, we can type:
~$ rails g serializer object
... and then ObjectsController will inherently know where to send the data to be serialized.
Serializing Associations
Now I'd wager that you're familiar with the belongs_to
, has_many
, and has_one
macros that you can use in model files to establish modular relationships (if not you can go here and I'll have your money the next time I get paid). Those can also be applied in serializers! Let's imagine some sort of card-collector app, where we have a User object that can have many Card objects...
class UserSerializer < ActiveModel::Serializer
attributes :id, :username
has_many :cards
end
class CardSerializer < ActiveModel::Serializer
attributes :id, :name, :year, :grade
belongs_to :user
end
Now a Card will be serialized with a nested User object, and a User object will be serialized with an array of that user's cards among its attributes. The nested data will of course have gone through its own serializer, but what if we don't want all of the provided attributes? We can use namespacing to achieve a more streamlined response...
class UserSerializer < ActiveModel::Serializer
attributes :id, :username
has_many :cards
class CardSerializer < ActiveModel::Serializer
attributes :name
end
end
... and our User object will contain an array of Card objects with only the name
attribute. Alternatively, a custom serializer can be created to accomplish the same result:
~$ rails g serializer user_card
class UserCardSerializer < ActiveModel::Serializer
attributes :name
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :username
has_many :cards, serializer: UserCardSerializer
end
Both methods will result in a User object that looks like this:
{
"id": 1,
"username": "mikeyb",
"cards": [
{
"name": "Mickey Mantle"
},
{
"name": "Charizard"
},
{
"name": "Tom Brady"
}
]
}
Now for the best part...
Custom Methods
That's right, we can pretty much do whatever we want to our data in here. Let's go back to our ObjectSerializer. On second thought, let's call it ItemSerializer to save us from some confusion down the line. We'll say that an Item has a weight attribute that's stored in the database as an integer. So right now, our JSON response will contain something like "weight": 65
. Seems kind of vague, right? Could be ounces, could be tons. We know that the numbers in the database represent kilograms, but how do we make that known in the view layer? How about we overwrite the weight attribute with a custom method?
class ItemSerializer < ActiveModel::Serializer
attributes :id, :name, :weight
def weight
"#{object.weight}kg"
end
end
And voila, our theoretical response body will now contain "weight": "65kg"
. You might be asking why we called object.weight
instead of self.weight
. This is because self
in this case refers to an instance of ItemSerializer. However, this instance does contain an "object" attribute, and inside of that is the Item instance. So to access the Item's weight, or any other attribute, we must type self.object.weight
(or just object.weight
).
Our newfound powers don't stop at just overwriting existing attributes, though. Not everyone subscribes to the metric system, so let's put an imperializer in our serializer!
class ItemSerializer < ActiveModel::Serializer
attributes :id, :name, :weight, :weight_in_lbs
def weight
"#{object.weight}kg"
end
def weight_in_lbs
pounds = object.weight * 2.20462262185
"#{pounds.round(1)} lbs"
end
end
Yes, we can essentially create our own attributes on the fly. An Item object sent to the client will now look something like:
{
"id": 1,
"name": "Coffee Table",
"weight": "65kg",
"weight_in_lbs": "143.3 lbs"
}
So are you using ActiveModel::Serializer yet? Don't you want to feel the power? Here's another link so you don't have to scroll all the way up. It's time to get serious about serializing.
Posted on September 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.