DEV Community

Naftali Kulik
Naftali Kulik

Posted on • Updated on

Using Active Model Serializer

Installing the Active Model Serializer gem

In order to use Active Model Serializer, you will need to include it in your Gemfile. Simply add gem "active_model_serializers" to your Gemfile and run bundle install. Alternatively, you can run bundle add active_model_serializers from the root directory of your app to accomplish the same thing.

Serializer Basics

As part of my ongoing journey toward becoming a full-stack developer, I have begun developing Rails API. Rails has many great built-in libraries to handle everything from organizing and modifying databases to setting up routes and controller actions to handle requests to those routes. However, using controller actions to handle the data sent back in response to those requests has its limitations. The controller's job is to process the requests sent by the client, which includes but is not limited to authentication, authorization, updating the database in response to the contents of the request body, and actually sending the response. Using the controller to sort through the available data and decide what to send back would grow your controller actions into large, unwieldy, and difficult-to-read blocks of code.

You may be wondering, who cares? Just send it all back and let the client sort it out! That's not too practical though. You may find yourself dealing with large databases with complex many-to-many relationships which require nested data to be sent to the client. Sending everything back in a large chunk for every request doesn't make much sense. Additionally, it may be possible that some of your controller actions require authorization, and if so there's a good chance there's also data that you only want available to authorized users. You wouldn't want, for example, users#index to send all of your users' email addresses and phone numbers! That may be fine for other controller actions (for a hypothetical User model) that require authorization, but not for one that's available for everyone.

This is where ActiveModel::Serializer comes in. The acitve_model_serializer gem allows you to move all that code into a separate file, which will be accessed by the controller either implicitly (if the serializer shares the name of the model that it is connected to) or if explicitly specified. The serializer can be easily created with a rails generator, like this: rails g serializer <serializer_name>. At its most basic, a serializer will include the table rows, as attributes, that are to be sent back in the response, something like this:

class UserSerializer < ActiveModel::Serializer
  attributes :id, :first_name, :last_name, :username, :bio, :avatar_url
end
Enter fullscreen mode Exit fullscreen mode

This is telling the controller to only include those attributes in the response. No sending the user's phone number to anyone who wants it! You can also define custom serializers. Let's say, using the above example, we wanted to send the user's full name as one string. we could write out a method that returns the full name, something like this:

def full_name
  "#{self.object.first_name} #{self.object.last_name}"
end
Enter fullscreen mode Exit fullscreen mode

In this case, self is the instance of UserSerializer, and object is an attribute that allows you to access the current instance of User that you are dealing with. By accessing the user, you can in turn access the data for that user and incorporate it into a method that returns whatever information that you want to be returned. Now all you have to do is add :full_name to the list of attributes and it will be sent as part of the JSON data of the response.

What about the example we used before, with certain controller actions which, upon authorization, send data such as personal user information back to the client? The serializer we have here doesn't send that information, which is good, but we do want the user to have access to it once they prove it's them. Our users#show action, for example, may need to send the user their profile information, including information private to them. Our create and update actions, which allow the user to modify their profile, also need to return more detailed and personalized data.

To accomplish this, we can generate a new serializer and explicitly specify in our controller to use this new one. We'll start with generating the new serializer: rails g serializer user_profile. Now we can include the attributes we want to send, and specify in the controller to use this serializer, which might look something like this:

render json: user, serializer: UserProfileSerializer
Enter fullscreen mode Exit fullscreen mode

In this specific case, it might actually make more sense for the custom serializer to be used for the index action, as that is the one that is apart from the others. In this case, we'd use each_serializer, like this:

render json: User.all, each_serializer: UserListSerializer
Enter fullscreen mode Exit fullscreen mode

The most powerful use of serializers may be the ability to leverage Active Record associations to send nested data. In its most simple form, including associated data is as simple as sticking a has_many (or whatever the relationship is) macro in the serializer (assuming the relationship is set up correctly in your models). You can even specify a separate serializer for the nested data if, and this is often going to be the case, the data you need to send as nested data is not necessarily what you'd want to send in response to a request to the nested resource itself. For example, a book has_many many chapters. You may want to send some basic chapter information with the book (such as the names), but you don't necessarily need a comprehensive collection of information about the chapter, such as its word count, subject, etc, although you would want to send that in response to a request to chapters itself.

Displaying Deeply Nested Data, My Way

You can also send data that's nested even deeper, but that's a bit complicated and probably should be avoided if possible. I've found that often when I've felt like I wanted to nest data more than one layer deep, there was usually a better way to do it. Particularly, combining the features of custom serializers and Active Record associations can often get the results you need without resorting to writing complex code to send back complex data. Let's look at an example from my most recent project, game-center (deployed here).

This app allows users to browse, like, rate, and review free online games (game information obtained via the Freetogame API). The Review model has some pretty complex associations. It belongs_to a User (which has_many reviews), belongs_to a Game (which has_many reviews, and has_many users through reviews), and belongs_to a Rating. I'd like to be able to display a detailed game, which would include a list of reviews, along with some information about the user. A review from this list as displayed in the browser may look something like this:

Sample Review
You may see the potential problem here. We need, in addition to the actual review content, information about the user (name, username, avatar) and the rating associated with this review. This is on top of the fact that the review itself is being sent as a nested resource! This could get complicated and impractical quickly. Allow me to demonstrate how a bit of creativity can make this quite simple.

First, add has_many :reviews to the GameSerializer. This will send whichever attributes are specified in the ReviewSerializer, not including nested attributes, as a nested attribute of the Game. Now we can use custom serializers and Active Record associations to customize our Review objects as needed. Let's start with the basic attributes:

class ReviewSerializer < ActiveModel::Serializer
  attributes :id, :content
end
Enter fullscreen mode Exit fullscreen mode

Assuming our client doesn't need access to the game_id and user_id, this is really all that needs to be sent back from the Review model itself. We also want the rating, which, as we've mentioned before, is a separate resource that the review belongs_to. Instead of trying to work out complicated nested attributes, let's let our association do the work for us in the form of a custom method:

def rating
  self.object.rating.rating
end
Enter fullscreen mode Exit fullscreen mode

Here we have self.object, the review itself, .rating, the instance of Rating associated with this review, and .rating, the attribute that has the actual value of the rating. In short, this method returns the value of the rating associated with this review, without resorting to sending nested and/or unnecessary data. Now we can just add :rating to our list of attributes and there we have it! Now let's do the same for the user information that we need:

def user_full_name
  "#{self.object.user.first_name} #{self.object.user.last_name}"
end

def user_username
  self.object.user.username
end

def user_avatar
  self.object.user.avatar_url
end

def user_tier
  self.object.user.tier
end
Enter fullscreen mode Exit fullscreen mode

Let's see the whole serializer at once:

class ReviewSerializer < ActiveModel::Serializer
  attributes :id, :game_id, :user_id, :content, :rating, :user_full_name, :user_username, :user_avatar, :user_tier

  def rating
    self.object.rating.rating
  end

  def user_full_name
    "#{self.object.user.first_name} #{self.object.user.last_name}"
  end

  def user_username
    self.object.user.username
  end

  def user_avatar
    self.object.user.avatar_url
  end

  def user_tier
    self.object.user.tier
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, the json response data for a game should look something like this:

{
    "id": 19,
    "title": "Game Of Thrones Winter Is Coming",
    "thumbnail": "https://www.freetogame.com/g/340/thumbnail.jpg",
    "description": "Game of Thrones Winter is Coming is a free-to-play browser-based RTS based on the George R.R. Martin novels and popular HBO series. In the game developed by YOOZOOGames, player take on the role of a Westeros lord set on putting a stop to the wars between the Seven Kingdoms.\r\n\r\nThe game is built in Unity and offers players a balanced mix of strategy and RP. Players build bases, grow their kingdom, train armies, and recruit characters from the show, all while living within the story set forth in the TV series.",
    "game_url": "https://www.freetogame.com/open/game-of-thrones-winter-is-coming",
    "genre": "Strategy",
    "platform": "Web Browser",
    "publisher": "GTArcade",
    "developer": "YOOZOO Games ",
    "release_date": "2019-11-14",
    "likes": 4,
    "rating": "3.4",
    "reviews": [
        {
            "id": 602,
            "game_id": 19,
            "user_id": 6,
            "content": "Id quia ut. Quas a accusamus. Non iusto et.",
            "rating": 4,
            "user_full_name": "King Hamill",
            "user_username": "OldRasputinRussianImperialStout",
            "user_avatar": "https://robohash.org/eligendiquaevoluptas.png?size=300x300&set=set1",
            "user_tier": "Hall of Fame"
        },
        {
            "id": 1093,
            "game_id": 19,
            "user_id": 10,
            "content": "Qui ut doloribus. Sapiente ut odit. Nemo placeat sequi.",
            "rating": 3,
            "user_full_name": "Virgilio Larson",
            "user_username": "OrvalTrappistAle",
            "user_avatar": "https://robohash.org/ducimusrepudiandaeet.png?size=300x300&set=set1",
            "user_tier": "Hall of Fame"
        },
        {
            "id": 1197,
            "game_id": 19,
            "user_id": 11,
            "content": "Molestias veniam beatae. Ex alias ut. Mollitia quasi natus.",
            "rating": 5,
            "user_full_name": "Jackson Lamar",
            "user_username": "QB1",
            "user_avatar": "https://robohash.org/facereminimavoluptatem.png?size=300x300&set=set1",
            "user_tier": "Hall of Fame"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

There we go! Nice, simple, and organized!

I have found that serializers can be an extremely powerful tool when used correctly, and I hope that I've demonstrated that here. Thanks for sticking around!

Latest comments (1)

Collapse
 
malkav profile image
Ilya Levin

One catch - the project hasn't been updated in 5-6 years.