Active Model Serializers in Rails API: Clean JSON Responses Made Simple

Learn how Active Model Serializers improves JSON serialization in Rails. Build cleaner APIs, control response structure, and simplify your JSON API design.

Ruby on Rails Expert

Gokul P

Lead Technical Consultant

Illustration of laptop with Api related icons on screen

Building a clean and predictable Rails API can quickly become challenging, especially when every endpoint starts returning inconsistent data or leaking fields that should’ve stayed internal.

And that’s when Active Model Serializers comes to the rescue. With the Active Model Serializers gem, you can transform messy model dumps into clear, maintainable, and structured JSON output.

Whether you're trying to streamline JSON serialization in Rails or simply want a better way to control the format of your API responses, Active Model Serializers gives you a powerful abstraction layer. It sits neatly between your Rails models and your API, letting you define exactly what gets exposed and how it’s delivered, making it easier than ever to implement Active Model Serializers in a Rails API and deliver predictable, frontend-friendly JSON.

What is Active Model Serializers?

Active Model Serializers (AMS) is a gem that controls how your Rails models convert to JSON.

Instead of exposing raw model data, you define exactly what is included in each response. You control attribute names, relationships, and data formats.

Think of it as a view layer for your API. Just like Rails views format HTML, serializers format JSON.

Why does it matter?

Raw model JSON causes real problems.

You expose sensitive data by accident. Password hashes, internal IDs, and timestamps that the frontend doesn't need all leak through.

Your API responses become coupled to the database structure. Changing a column name breaks every client.

You write the same formatting logic repeatedly. Date formatting, nested associations, and computed values are all duplicated across controllers.

Active Model Serializers solves these issues. You define the structure once. Every endpoint using that model follows the same format.

Your API becomes predictable. Frontend developers know exactly what to expect. Documentation stays accurate because the serializer defines the contract.

How to implement Active Model Serializers?

Start by adding the gem and building your first serializer.

Step 1: Install the gem

Add AMS to your Gemfile:

gem 'active_model_serializers', '~> 0.10.0' 

Run bundle install:

bundle install 

Step 2: Generate a serializer

Rails generators make this quick:

rails g serializer User 

This creates

app/serializers/user_serializer.rb: 

class UserSerializer < ActiveModel::Serializer 

attributes :id, :email, :name 

end 

Step 3: Choose your attributes

Edit the serializer to include only what you need:

class UserSerializer < ActiveModel::Serializer 

 attributes :id, :email, :full_name, :created_at 

 def full_name 

 "#{object.first_name} #{object.last_name}" 

 end 
 
def created_at 

 object.created_at.strftime('%Y-%m-%d') 

 end 

end 

The object variable refers to your model instance. You can call any model method or define custom ones.

Step 4: Handle associations

Add relationships between models:

class UserSerializer < ActiveModel::Serializer 

 attributes :id, :email, :full_name 

 has_many :posts 

 belongs_to :company 

end 

Create serializers for associated models:

rails g serializer Post 

rails g serializer Company 

Define what each association shows:

class PostSerializer < ActiveModel::Serializer 

  attributes :id, :title, :published_at 
  
  belongs_to :user 

end 

Step 5: Use in controllers

Your controller stays clean:

class Api::V1::UsersController < ApplicationController 

  def index 

    users = User.all 

    render json: users 

  end 
  def show 

    user = User.find(params[:id]) 

    render json: user 

  end 

end 

 

AMS automatically uses the serializer. No extra code needed.

Step 6: Customize per endpoint

Sometimes you need different data for different endpoints:

def show 


user = User.find(params[:id]) 

render json: user, include: ['posts', 'company'] 
  
end 

def index 

users = User.all 

 render json: users, fields: { users: [:id, :email] } 

end 

The include parameter controls associations. The fields parameter limits attributes.

Step 7: Add conditional attributes

Show data based on permissions:

class UserSerializer < ActiveModel::Serializer 

 attributes :id, :email, :full_name 

 attribute :phone_number, if: :show_phone? 

  attribute :ssn, if: :admin_user? 
   
  def show_phone? 

  current_user&.premium? 

  end 
  
  def admin_user? 

  current_user&.admin? 

  end 

 def current_user 

 scope 
    
  end 

  end 

Pass the current user when rendering:

render json: user, scope: current_user 

Common mistakes to avoid

Not creating serializers for associations. If User has_many :posts but you don't create PostSerializer, you get raw model JSON for posts.

Calling database queries inside serializers. This creates N+1 problems. Use eager loading in your controller instead:

users = User.includes(:posts, :company).all 

Over-serializing associations. Loading every nested relationship kills performance. Be selective:

render json: users, include: ['posts']  # Good 
render json: users, include: ['**']  # Bad - loads everything 

Forgetting to handle nil associations. Add guards in custom methods:

def company_name 

 object.company&.name || 'No company' 

end 

Using serializers for input validation. Serializers format output only. Use strong parameters for input.

Testing your changes

Test serializers like any Ruby class:

# spec/serializers/user_serializer_spec.rb 

require 'rails_helper' 

RSpec.describe UserSerializer do 

  let(:user) { create(:user, first_name: 'John', last_name: 'Doe') } 

  let(:serializer) { UserSerializer.new(user) } 

  let(:serialization) { ActiveModelSerializers::Adapter.create(serializer) } 

  it 'includes expected attributes' do 

    json = serialization.as_json 

    expect(json[:user][:full_name]).to eq('John Doe') 

    expect(json[:user]).to have_key(:email) 

    expect(json[:user]).not_to have_key(:password_digest) 

  end 

end 


Test in controllers with request specs:

# spec/requests/api/v1/users_spec.rb 

require 'rails_helper' 

RSpec.describe 'Users API' do 

it 'returns serialized user data' do 

    user = create(:user) 

    get "/api/v1/users/#{user.id}" 

    json = JSON.parse(response.body) 

   expect(json['user']['full_name']).to be_present 

   expect(json['user']['password_digest']).to be_nil 

  end 

end 

Check your API responses manually:

curl http://localhost:3000/api/v1/users/1 | jq 

The jg tool formats JSON for easy reading.

Final words

In the end, what makes an API great isn’t the JSON it sends, it is all about how meaningful and well-structured that JSON is. Using a Rails serializer through the Active Model Serializers gem ensures your API stays organized, consistent, and easy for clients to consume. Instead of scattering formatting logic across controllers, you define your data contract once and reuse it everywhere.

If you're aiming to improve JSON API using Active Model Serializers with Rails, AMS offers the structure, flexibility, and clarity needed to keep your code clean and scalable. For teams looking to implement Active Model Serializers with Rails API the right way or untangle messy JSON responses, RailsFactory can help you build an API that’s easier to maintain and far more enjoyable to work with.

Or if you simply need expert guidance on any Ruby on Rails or broader software development needs, contact our team. We are always here to help.

FAQs

1. What's the difference between Active Model Serializers and Jbuilder?

Active Model Serializers uses Ruby classes to define JSON structure. Jbuilder uses template files similar to views. AMS tends to be faster and more object-oriented. Jbuilder gives more flexibility for complex, one-off formats. Choose AMS for consistent API responses. Choose Jbuilder when you need template-style control.

2. How do I version my API with serializers?

Create separate serializers for each version:

# app/serializers/api/v1/user_serializer.rb 

class Api::V1::UserSerializer < ActiveModel::Serializer 

 attributes :id, :name 

end 

# app/serializers/api/v2/user_serializer.rb 

class Api::V2::UserSerializer < ActiveModel::Serializer 

 attributes :id, :full_name, :email 

end 

Specify the serializer explicitly in controllers:

render json: user, serializer: Api::V2::UserSerializer 

3. Can I use Active Model Serializers with GraphQL?

Not directly. GraphQL has its own type system. The gem works best with REST APIs. For GraphQL in Rails, use the graphql-ruby gem instead. It provides similar control over data structure but follows GraphQL patterns.

4. How do I handle pagination with serializers?

AMS works with pagination gems like Kaminari or will_paginate:

users = User.page(params[:page]).per_page(20) 

  render json: users, meta: {  

 current_page: users.current_page, 

 total_pages: users.total_pages, 

 total_count: users.total_count 

 } 

The meta data appears in your JSON response alongside the serialized users.

Written by Gokul P

Gokul is a Lead Technical Consultant at RailsFactory with over 10 years of experience architecting scalable Ruby on Rails applications for high-growth teams. He specializes in Rails, React, and AWS, building robust enterprise solutions.

Other blogs

You may also like


Your one-stop shop for expert RoR services

join 250+ companies achieving top-notch RoR development without increasing your workforce.