Introduction
Every ActiveRecord model can be rendered as JSON with render json: @model. By default, this exposes all database columns. The as_json method lets you control exactly which fields appear in your API responses.
Key Concepts
as_json: Method called when converting an object to JSON. Override it on models to customize the output.only/except: Options to whitelist or blacklist specific attributes.methods: Option to include computed method results in the JSON output.include: Option to nest associated models in the response.
Real World Context
Without customizing as_json, your API will expose internal fields like password_digest, created_at, updated_at, and foreign keys. This is both a security risk and poor API design. Every production API needs explicit control over response shapes.
Deep Dive
Basic Customization
rubyclass Product < ApplicationRecord belongs_to :category has_many :reviews def as_json(options = {}) super(options.merge( only: [:id, :name, :price, :description, :stock_count], methods: [:formatted_price, :average_rating], include: { category: { only: [:id, :name] } } )) end def formatted_price "$#{'%.2f' % (price.to_f / 100)}" end def average_rating reviews.average(:rating)&.round(1) || 0.0 end end
Now render json: @product outputs only the specified fields plus computed values. Internal columns like created_at and category_id are excluded.
Controller-Level Customization
You can also customize at the controller level:
rubydef show product = Product.find(params[:id]) render json: product.as_json( only: [:id, :name, :price], include: { reviews: { only: [:id, :body, :rating], include: { author: { only: [:id, :name] } } } } ) end
Controller-level customization overrides the model default, giving you endpoint-specific shapes.
Collection Rendering
For collections, each item uses the same as_json rules:
rubydef index products = Product.includes(:category).all render json: { data: products, meta: { total: products.count } } end
Each product in the array is serialized using the model's as_json method. The includes(:category) prevents N+1 queries.
Common Pitfalls
- N+1 queries with
include— Usinginclude:inas_jsonwithoutincludes()in the query triggers a database query per record. - Forgetting to whitelist fields — Without
only:, all columns are exposed.
Best Practices
- Always use
only:to whitelist — Explicit field listing prevents accidental exposure of sensitive data. - Eager load associations — Always use
.includes()when your JSON includes associations.
Summary
- Override
as_jsonon models to control default JSON representation. - Use
only:to whitelist fields,methods:for computed values,include:for associations. - Controller-level
as_jsonoptions override model defaults. - Always eager load associations to prevent N+1 queries.
Code Examples
class User < ApplicationRecord
def as_json(options = {})
super(options.merge(
only: [:id, :name, :email],
methods: [:avatar_url, :posts_count]
))
# Never exposes: password_digest, api_token,
# remember_digest, admin flag, etc.
end
end