DEV Community

Donte Ladatto
Donte Ladatto

Posted on

ActiveModel::Serializer and You

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

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

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

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

    ... 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
Enter fullscreen mode Exit fullscreen mode
class UserCardSerializer < ActiveModel::Serializer
  attributes :name
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :username
  has_many :cards, serializer: UserCardSerializer
end
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

    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.

Top comments (0)