ActiveModel::Serializer and You
Tags: beginners, ruby, rails, webdev
Published: 2022-09-23 17:21:29

&nbsp;&nbsp;&nbsp;&nbsp;Today I thought I'd run through a few of the powerful things that the [ActiveModel::Serializer gem](https://github.com/rails-api/active_model_serializers/tree/0-10-stable) brings to the table when used in a Ruby on Rails application. Sure, there are [faster](https://github.com/jsonapi-serializer/jsonapi-serializer) [alternatives](https://github.com/procore/blueprinter), 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?
&nbsp;&nbsp;&nbsp;&nbsp;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:  
```ruby
class ObjectsController < ApplicationController

  def index
    objects = Object.all
    render json: objects, except: [:created_at, :updated_at]
  end

end
```
&nbsp;&nbsp;&nbsp;&nbsp;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
&nbsp;&nbsp;&nbsp;&nbsp;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:
```console
~$ rails g serializer object
```
&nbsp;&nbsp;&nbsp;&nbsp;... and then **ObjectsController** will inherently know where to send the data to be serialized.

###Serializing Associations
&nbsp;&nbsp;&nbsp;&nbsp;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](https://guides.rubyonrails.org/association_basics.html) 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...
```ruby
class UserSerializer < ActiveModel::Serializer
  attributes :id, :username
  has_many :cards
end

class CardSerializer < ActiveModel::Serializer
  attributes :id, :name, :year, :grade
  belongs_to :user
end
```
&nbsp;&nbsp;&nbsp;&nbsp;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...
```ruby
class UserSerializer < ActiveModel::Serializer
  attributes :id, :username
  has_many :cards
  class CardSerializer < ActiveModel::Serializer
    attributes :name
  end
end
```
&nbsp;&nbsp;&nbsp;&nbsp;... 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:
```console
~$ rails g serializer user_card
```
```ruby
class UserCardSerializer < ActiveModel::Serializer
  attributes :name
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :username
  has_many :cards, serializer: UserCardSerializer
end
```
&nbsp;&nbsp;&nbsp;&nbsp;Both methods will result in a **User** object that looks like this:
```json
{
  "id": 1,
  "username": "mikeyb",
  "cards": [
    {
      "name": "Mickey Mantle"
    },
    {
      "name": "Charizard"
    },
    {
      "name": "Tom Brady"
    }
  ]
}
```
&nbsp;&nbsp;&nbsp;&nbsp;Now for the best part...

###Custom Methods
&nbsp;&nbsp;&nbsp;&nbsp;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?
```ruby
class ItemSerializer < ActiveModel::Serializer
  attributes :id, :name, :weight

  def weight
    "#{object.weight}kg"
  end
end
```
&nbsp;&nbsp;&nbsp;&nbsp;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`).
&nbsp;&nbsp;&nbsp;&nbsp;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!
```ruby
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
```
&nbsp;&nbsp;&nbsp;&nbsp;Yes, we can essentially create our own attributes on the fly. An **Item** object sent to the client will now look something like:
```json
{
  "id": 1,
  "name": "Coffee Table",
  "weight": "65kg",
  "weight_in_lbs": "143.3 lbs"
}
```

&nbsp;&nbsp;&nbsp;&nbsp;So are you using ActiveModel::Serializer yet? Don't you want to feel the power? [Here](https://github.com/rails-api/active_model_serializers/tree/0-10-stable)'s another link so you don't have to scroll all the way up. It's time to get serious about serializing.