A quick intro to Rails Serializers

From the basics to the slightly less basic

Max Powell
8 min readJul 19, 2019

As good as Rails is for building an API, it’s default responses leave something to be desired. That’s why we use Active Model Serializers to structure the data we send back to the client appropriately.

To see why it’s important to be able to customise the structure of the data your API sends through, let’s run through an example without our serializers.

I’m building a chat app. I have three models: User, Chat and Message.

class User < ApplicationRecord
has_many :messages
has_and_belongs_to_many :chats
end
class Chat < ApplicationRecord
has_many :messages, dependent: :destroy
has_and_belongs_to_many :users
end
class Message < ApplicationRecord
belongs_to :chat
belongs_to :user
end

When a user logs in, as well as their information, I need to pull all their chats and the messages within those chats. So my user logs in and…

{
"id": 1,
"username": "Max",
"password_digest": "$2a$10$bg9lByNrfpA/kMALSP7xkuRvzd4GFrGSuwU.Vl.m34ZqijrH9sTWa",
"created_at": "2019-07-10T11:24:13.766Z",
"updated_at": "2019-07-10T11:24:13.766Z"
}

…ok. Well I’ve got the user id, so I can pull through their chats using that…

[
{
"id": 1,
"title": "First chat",
"created_at": "2019-07-10T11:25:30.702Z",
"updated_at": "2019-07-10T11:25:30.702Z"
},
{
"id": 2,
"title": "Second chat",
"created_at": "2019-07-16T09:25:42.203Z",
"updated_at": "2019-07-16T09:25:42.203Z"
},
{
"id": 3,
"title": "Third chat",
"created_at": "2019-07-16T20:36:53.324Z",
"updated_at": "2019-07-16T20:36:53.324Z"
}
]

Oh, come on! So now I’ve got to send a request for the messages from each of these chats! All in, that will have been five requests to get the details of three chats.

Serializers allow us to specify that, when we request a user’s information, we get their chats and messages as well. So five requests becomes one.

Let’s see how we can start putting this into practice.

1. The absolute basics

First things first, we need to install the gem: gem 'active-model-serializer && bundle i.

Now we can start creating some serializers:

rails g serializer user
rails g serializer chat
rails g serializer message

The generator function gives us three serializers that look like this:

# CHAT_APP/app/serializers/user_serializer.rbclass UserSerializer < ActiveModel::Serializer
attributes :id
end
# CHAT_APP/app/serializers/chat_serializer.rbclass ChatSerializer < ActiveModel::Serializer
attributes :id
end
# CHAT_APP/app/serializers/message_serializer.rbclass MessageSerializer < ActiveModel::Serializer
attributes :id
end

These serializers will now be the default structure for their relevant model when render is called in the controller (thanks to some nifty Rails naming magic). Now when I request my user data I get this:

{
"id": 1
}

Not great. We need to start building out what we actually want to see from our API.

Only the attributes specified in the attribute whitelist will be sent through in our responses, so let’s add to it.

# CHAT_APP/app/serializers/user_serializer.rbclass UserSerializer < ActiveModel::Serializer
attributes :id, :username
end

This serializer will pass us the user’s id and username. I definitely do not want their password digest sent through, so I can omit by simply not adding it to list. Same with the timestamps.

I can do something similar with the Chat and Messages serializers to ensure I get a chat’s title and a message’s content.

2. Custom attributes

Suppose we want to know if a user has recently created their account; less than a month ago, let’s say. It’s not an attribute that my User instance has, so I’ll need to create it in the serializer.

# CHAT_APP/app/serializers/user_serializer.rbrequire 'date'class UserSerializer < ActiveModel::Serializer
attributes :id, :username, :recently_joined?
def recently_joined?
Date.today.prev_month < object.created_at
end
end

So, what’s happened here? First, I added my new recently_joined? attribute to my whitelist of attributes. Doing that alone would throw an error as our User object doesn’t have that attribute. But since I’ve then defined it in the body of the serializer, the API will run that method and use its return value as the value of the attribute.

A couple of things to note here:

  1. Within a serializer, the keyword object represents what ever object has been passed to the serializer. In the above example, object represents our user.
  2. The serializer will look for its own methods before checking the objects attributes. This means that you could override an object’s attributes if you define a method in the serializer with the same name.

We can also make some information conditional, like if we only want to send some information if the user had recently joined the app:

# CHAT_APP/app/serializers/user_serializer.rbrequire 'date'class UserSerializer < ActiveModel::Serializer
attributes :id, :username
attribute :tutorial_complete?, if: :recently_joined?
def recently_joined?
Date.today.prev_month < object.created_at
end
end

3. Associations

Right now, our objects are coming through with more relevant data, but we still need to send multiple requests to get everything we need. Helpfully, serializers allow us to specify what associations an object has, in the same way we do when we write out our models.

# CHAT_APP/app/serializers/user_serializer.rbclass UserSerializer < ActiveModel::Serializer
attributes :id, :username
has_many :chats
end

Now, when we request our user information, we get this:

{
"id": 1,
"username": "Max",
"chats": [
{
"id": 1,
"title": "First chat"
},
{
"id": 2,
"title": "Second chat"
},
{
"id": 3,
"title": "Third chat"
}
]
}

That’s more like it. Now we just need to specify that a chat has many messages and…

{
"id": 1,
"username": "Max",
"chats": [
{
"id": 1,
"title": "First chat"
},
{
"id": 2,
"title": "Second chat"
},
{
"id": 3,
"title": "Third chat"
}
]
}

Nothing happens. Serializers, by default, only handle one level of nesting. There’s a few ways around this. We could create a custom attribute that returns the objects associations, like so:

# CHAT_APP/app/serializers/user_serializer.rbclass ChatSerializer < ActiveModel::Serializer
attributes :id, :title, :messages
def messages
object.messages
end
end

Or we could create a new initializer file for serializers and change their configuration like so:

# CHAT_APP/config/initializers/active_model_serializer.rbActiveModelSerializers.config.default_includes = '**'

Or we could specify exactly what we want to include when we call render in the controller:

# CHAT_APP/app/controllers/users_controller.rb  def show
render json: current_user, include: ['chats', 'chats.messages']
end

They all give you the same result, but I prefer the last one, as it’s clean but gives me a lot of control.

Now our request returns us something like this:

"id": 1,
"username": "Max",
"chats": [
{
"id": 1,
"title": "First chat",
"messages": [
{
"id": 1,
"text": "Hi",
"chat_id": 1,
"created_at": "2019-07-10T11:26:34.328Z"
}
]
},
{
"id": 2,
"title": "second chat",
"messages": [
{
"id": 99,
"text": "Hi again",
"chat_id": 1,
"created_at": "2019-07-16T09:20:50.269Z"
},
{
"id": 100,
"text": "How's it?",
"chat_id": 1,
"created_at": "2019-07-16T09:20:59.553Z"
}
]
},
{
"id": 3,
"title": "Third chat",
"messages": [
{
"id": 119,
"text": "Hi",
"chat_id": 4,
"created_at": "2019-07-16T20:37:32.778Z"
}
]
}
]
}

Great. I think we’re on to a winner.

4. Custom serializers

So I want to add two more things to my app. First, some personal information to my users — that’s easy enough. Second, I need to do something about those messages. Right now, I can see the content of a message and when it was sent, but I have no idea who sent it. How can anyone have a chat if they don’t know who sent what message?

So I just need to add a belongs_to :user association to my message serializer…

# CHAT_APP/app/serializers/message_serializer.rbclass MessageSerializer < ActiveModel::Serializer
attributes :id, :text, :chat_id, :created_at
belongs_to :user
end

…and we’re good to go:

"id": 1,
"username": "Max",
"personal_information_DO_NOT_SHARE": {
"email": "max@example.com",
"phone": "0123456789"
},
"chats": [
{
"id": 1,
"title": "First chat",
"messages": [
{
"id": 1,
"text": "Hi",
"chat_id": 1,
"created_at": "2019-07-10T11:26:34.328Z",
"user": {
"id": 2,
"username": "Test",
"personal_information_DO_NOT_SHARE": {
"email": "test@example.com",
"phone": "0123498765"
}

That’s not good. I’ve managed to send my user the personal information of every other user they’ve had a chat with. If you’ll excuse me, I have a GDPR case that I need to prepare a defence for.

But in the meantime, I should probably fix this. The issue is that any User object will be passed to the user serializer by default, and I’ve set that serializer up to handle the current user.

I need to pass the other user object somewhere else, where I can limit what information is shown about them. So I’ll create a new serializer to handle users when they are the sender of a message:

rails g serializer sender# CHAT_APP/app/serializers/sender_serializer.rbclass SenderSerializer < ActiveModel::Serializer
attributes :id, :username
end

And I need to make sure that this is the serializer that is used when for the user a message belongs to:

# CHAT_APP/app/serializers/message_serializer.rbclass MessageSerializer < ActiveModel::Serializer
attributes :id, :text, :chat_id, :created_at
belongs_to :user, serializer: SenderSerializer
end

And now, hopefully, I won’t compromise any more users’ data:

"id": 1,
"username": "Max",
"personal_information_DO_NOT_SHARE": {
"email": "max@example.com",
"phone": "0123456789"
},
"chats": [
{
"id": 1,
"title": "First chat",
"messages": [
{
"id": 1,
"text": "Hi",
"chat_id": 1,
"created_at": "2019-07-10T11:26:34.328Z",
"user": {
"id": 2,
"username": "Test"
}

Success!

It’s useful to know that this method can be used outside in the controller as well:

e.g.# CHAT_APP/app/controllers/users_controller.rb  def show
user = User.find(params[:id])
render json: user, serializer: SenderSerializer
end
def index
users = User.all
render json: users, each_serializer: SenderSerializer
end

Note that, if you want to render a collection of resources, you need to use each_serializer: instead of serializer:.

5. Using serializers without rendering

In our controller, when we call the render function, it will automatically pass the object to a serializer (if one exists), either using implicit naming conventions, or if we’ve explicitly declared one.

But we don’t need to call the render function to use our serializers. We can pass objects to them ourselves to create a serialized hash of the object:

ActiveModelSerializers::SerializableResource.new(message, {serializer: MessageSerializer}).as_json

We are going to need this for our chat app because, when a user sends a new message, we aren’t going to give the standard HTTP response. Instead, we will be using a TCP connection to send the message out to anyone in the chat. To do this, we will use ActionCable (Rails’ default implementation of WebScokets) and its broadcast_to method.

The broadcast_to method doesn’t call any serializers, so we will have to serialize the messages ourselves before we the send them out. The result looks like this:

# CHAT_APP/app/controllers/messages_controller.rbdef create
message = Message.new(message_params)
chat = Chat.find(params[:chat_id])
message.user = current_user
message.chat = chat
if message.save
serialized_message =
ActiveModelSerializers::SerializableResource.new(message,
{serializer: MessageSerializer}).as_json

MessagesChannel.broadcast_to chat, serialized_message
head :ok
else
render json: {error: 'Unable to send message'}
end
end

We create a new serializable resource and then conver it into JSON. We can then pass that to our broadcast_to method to send it out to our users.

This is a far from comprehensive guide for Rails Serializers. The full documentation can be found here. But hopefully this has covered most of what you need to know to get on and build your own API.

--

--