DevConvert
Ruby on Rails

Rails JSON Serialization: as_json, to_json, JBuilder, and Serializers Explained

A complete guide to Rails JSON serialization — when to use as_json vs to_json, the role of JBuilder and Active Model Serializers, performance considerations, and the ActiveRecord debug problem.

8 min readMarch 2026

Need to convert right now? The tool is free — no signup required.


Rails has multiple ways to serialize ActiveRecord models to JSON, each with different trade-offs. Choosing the wrong one leads to over-fetching, N+1 queries, or leaking sensitive attributes. This guide covers all the approaches and when to use each.


The Three Layers


Rails JSON serialization happens at three levels:


1. **Model level** — what data an object exposes (as_json, to_json, serializable_hash)

2. **View/Template level** — how a response is shaped (JBuilder, ERB with render json:)

3. **Serializer level** — dedicated objects that define how a model maps to JSON (Active Model Serializers, Blueprinter, Panko)


Understanding which layer you're working in prevents most serialization bugs.


as_json vs to_json


These are the two most commonly confused methods.


**to_json** serializes an object to a JSON string:

user.to_json

# => '{"id":1,"name":"Alice","email":"[email protected]","created_at":"2024-01-01T10:00:00.000Z"}'


**as_json** returns a Ruby Hash (the intermediate representation before serialization):

user.as_json

# => {"id"=>1, "name"=>"Alice", "email"=>"[email protected]", "created_at"=>"2024-01-01T10:00:00.000Z"}


to_json calls as_json internally, then converts the hash to a JSON string.


Why does this matter? When you want to customize the output, override as_json:


def as_json(options = {})

super(options.merge(only: [:id, :name], methods: [:full_name]))

end


The only:, except:, include:, and methods: options on as_json control what's included.


The Attribute Leak Problem


The most common security mistake in Rails APIs: forgetting to restrict which attributes serialize.


By default, ActiveRecord's as_json includes every column — including password_digest, reset_password_token, admin, stripe_customer_id, and any other sensitive attribute.


Always use only: or except: in production:


user.as_json(only: [:id, :name, :email, :created_at])


Or override as_json in the model to enforce safe defaults.


Including Associations


The include: option fetches and serializes associations:


user.as_json(

only: [:id, :name],

include: {

posts: { only: [:id, :title, :published_at] }

}

)


Warning: This triggers an N+1 query if you call it on a collection without eager loading. Always use includes() in the query:


users = User.includes(:posts).all

users.map { |u| u.as_json(include: :posts) }


The render json: Shortcut


The simplest way to render JSON in a controller:


def show

render json: @user

end


This calls to_json on @user internally. But it gives you no control over attributes unless you've overridden as_json on the model.


A slightly better pattern:


def show

render json: @user.as_json(only: [:id, :name, :email])

end


JBuilder


JBuilder (included in Rails by default) uses a DSL in .jbuilder view templates to define the JSON shape:


# app/views/api/users/show.json.jbuilder

json.id @user.id

json.name @user.name

json.email @user.email

json.created_at @user.created_at.iso8601


json.posts @user.posts do |post|

json.id post.id

json.title post.title

end


JBuilder separates JSON structure from model logic, which is clean for complex nested responses. The downside: it's slow at scale (template rendering overhead per request) and hard to test without hitting the full render stack.


Active Model Serializers


AMS (Active Model Serializers) uses dedicated serializer classes:


class UserSerializer < ActiveModel::Serializer

attributes :id, :name, :email, :created_at

has_many :posts, serializer: PostSerializer

end


In the controller:


render json: UserSerializer.new(@user)


AMS is more maintainable than overriding as_json and more testable than JBuilder. But it has been effectively abandoned by its maintainers and has known performance issues with large collections.


Modern Alternatives: Blueprinter and Panko


**Blueprinter** is a lightweight, fast alternative to AMS:


class UserBlueprint < Blueprinter::Base

identifier :id

fields :name, :email, :created_at

association :posts, blueprint: PostBlueprint

end


UserBlueprint.render(@user)


**Panko** is built for performance — it uses native extensions to serialize ActiveRecord objects much faster than AMS or JBuilder, making it suitable for endpoints returning thousands of records.


The Debug Problem: Console Output ≠ JSON


When debugging Rails, you work in the Rails console and see:


> User.first

=> #<User id: 1, name: "Alice", email: "[email protected]", role: "admin", created_at: Mon, 01 Jan 2024 10:00:00.000000000 UTC +00:00>


This is ActiveRecord's inspect output — not JSON. It's useful for reading but can't be pasted into Postman, a curl command, or a JSON schema validator.


The two common solutions in the console:

- User.first.to_json — gives JSON string

- User.first.as_json — gives a Ruby hash with string keys


But when you've copied output from a log file, a Slack message, or a system that doesn't have a running Rails console — you need a converter. That's the use case for the ActiveRecord → JSON tool.


Choosing the Right Approach


For small projects and simple APIs: render json: with explicit as_json options. Fast to implement, easy to understand.


For medium projects with consistent API shapes: Blueprinter or a simple serializer pattern. Testable, maintainable, fast enough.


For high-throughput APIs serializing large collections: Panko or database-side JSON aggregation (json_agg in PostgreSQL).


For complex, highly varied response shapes: JBuilder, but only if you're not under performance pressure.


Whatever you choose, always restrict attributes explicitly. Never let Rails serialize an entire model without specifying what's safe to expose.


Try the ActiveRecord → JSON

Free, instant, and no signup required.