JSON in Ruby on Rails: render json:, API Mode, Jbuilder & Active Model Serializers
Last updated:
Ruby on Rails renders JSON from any controller action with render json: object — passing an ActiveRecord model, array, or plain Ruby hash produces a Content-Type: application/json response. render json: user, only: [:id, :name, :email] whitelists which attributes appear, preventing password hashes and internal fields from leaking into API responses. Rails automatically parses Content-Type: application/json request bodies into params, so params[:user] works the same for JSON and form-encoded requests. Rails --api mode strips 26 unused middleware layers for pure JSON APIs. jbuilder (included by default) provides ERB-style JSON template files for complex response shapes. This guide covers render json:, field whitelisting with only:/except:, reading JSON request bodies, --api mode, Jbuilder templates, Active Model Serializers, and returning standard JSON error responses.
render json: — Returning JSON from Rails Controllers
render json: is the core Rails mechanism for returning JSON responses. It accepts any Ruby object — ActiveRecord models, arrays, hashes, or any object that responds to as_json — serializes it with JSON.generate, and sets the Content-Type: application/json header automatically. No middleware configuration or manual header setting is needed.
# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
before_action :set_user, only: [:show, :update, :destroy]
# GET /api/v1/users
def index
@users = User.all
render json: @users
# → Content-Type: application/json
# → [{"id":1,"name":"Alice","email":"alice@example.com","password_digest":"$2a$..."}]
# ⚠ Exposes password_digest — use only: in production
end
# GET /api/v1/users/:id
def show
render json: @user
# Rails calls @user.as_json internally
end
# POST /api/v1/users
def create
@user = User.new(user_params)
if @user.save
render json: @user, status: :created
# HTTP 201 Created + JSON body
else
render json: { errors: @user.errors.full_messages },
status: :unprocessable_entity
# HTTP 422 + error array
end
end
# DELETE /api/v1/users/:id
def destroy
@user.destroy
head :no_content
# HTTP 204 — no body, correct for DELETE success
# never: render json: nil (serializes as "null")
end
# Returning a plain hash — no model needed
def health
render json: { status: "ok", time: Time.current.iso8601 }
end
# Returning an array of hashes
def stats
render json: [
{ metric: "users_count", value: User.count },
{ metric: "active_today", value: User.where("last_seen > ?", 1.day.ago).count },
]
end
# respond_to for content-type negotiation
# (same controller serves HTML and JSON)
def dashboard
@data = build_dashboard_data
respond_to do |format|
format.html # → renders dashboard.html.erb
format.json { render json: @data }
end
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email, :password)
end
endBy default, render json: serializes all columns returned by ActiveRecord — including password_digest, reset_password_token, and any other sensitive fields. Always use only: in the next section to control which fields are exposed. The status: option accepts Ruby symbols (:created, :not_found) or integer codes (201, 404). head :no_content sends HTTP 204 with no body — the semantically correct response for successful DELETE, and lighter than render json: {}.
Filtering Fields with only: and except:
only: whitelists the fields included in the JSON output; except: blacklists specific fields. only: is the safer default — it silently ignores any new database column added later, preventing accidental data leaks. except: is convenient for excluding a few known-sensitive columns but will expose any new column added to the table.
# ── only: — whitelist specific fields ─────────────────────────
# Safer: new columns are ignored by default
render json: @user, only: [:id, :name, :email]
# → {"id":1,"name":"Alice","email":"alice@example.com"}
# password_digest, reset_password_token, etc. silently omitted
# ── except: — blacklist sensitive fields ───────────────────────
# Riskier: future columns are exposed automatically
render json: @user, except: [:password_digest, :reset_password_token,
:remember_digest, :admin]
# → exposes any new column added to users table
# ── methods: — include virtual attributes ──────────────────────
# Include Ruby methods as if they were database columns
class User < ApplicationRecord
def full_name
"#{first_name} #{last_name}"
end
def account_age_days
(Date.today - created_at.to_date).to_i
end
end
render json: @user,
only: [:id, :email],
methods: [:full_name, :account_age_days]
# → {"id":1,"email":"alice@example.com","full_name":"Alice Smith","account_age_days":42}
# ── include: — serialize associations ─────────────────────────
# Include a belongs_to association with field filtering
render json: @post,
only: [:id, :title, :body, :published_at],
include: {
author: { only: [:id, :name] },
tags: { only: [:id, :name] }
}
# → {"id":5,"title":"Hello","body":"...","published_at":"...","author":{...},"tags":[...]}
# ── as_json on the model — reusable serialization ──────────────
# Define a custom as_api_json method on the model
class User < ApplicationRecord
def as_api_json
{
id: id,
name: name,
email: email,
created_at: created_at.iso8601,
}
end
end
# Use it across controllers without repeating only:
render json: @users.map(&:as_api_json)
# ── Rendering collections with field filtering ─────────────────
render json: @users, only: [:id, :name, :email]
# Works on ActiveRecord::Relation — applies only: to every record
# ── Root wrapping ──────────────────────────────────────────────
# If include_root_in_json is enabled (off by default):
render json: @user, root: "user"
# → {"user":{"id":1,"name":"Alice",...}}For APIs consumed by external clients or mobile apps, define the set of safe public fields once in a model method or serializer rather than scattering only: arrays across every controller action. When the public field set changes, you update one place. The methods: option is useful for calculated attributes like full_name, avatar_url, or is_active that are not database columns but are part of the API contract.
Reading JSON Request Bodies via params
Rails automatically parses incoming application/json request bodies into the params hash via ActionDispatch::Request::Utils.deep_munge and ParamsParser middleware. Strong parameters work identically for JSON and form-encoded bodies — the controller code does not need to know which content type was used.
# Client sends:
# POST /api/v1/users
# Content-Type: application/json
# Body: {"user":{"name":"Alice","email":"alice@example.com","role":"admin"}}
# ── Strong parameters — same as form data ──────────────────────
def create
@user = User.new(user_params)
# params[:user] = {"name"=>"Alice","email"=>"alice@example.com","role"=>"admin"}
# user_params filters out :role (not permitted)
if @user.save
render json: @user, only: [:id, :name, :email], status: :created
else
render json: { errors: @user.errors.full_messages },
status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
# :role is stripped — never reaches the model
end
# ── Top-level JSON (no root key) ────────────────────────────────
# Client sends: {"name":"Alice","email":"alice@example.com"}
# Rails 5.1+ wraps top-level keys under the controller name
# params = {"name"=>"Alice","email"=>"alice@example.com","user"=>{...}}
# Access via: params.permit(:name, :email)
# ── Nested arrays in JSON body ─────────────────────────────────
# Client sends: {"order":{"line_items":[{"product_id":1,"qty":2}]}}
def order_params
params.require(:order).permit(:total, line_items: [:product_id, :qty])
# line_items: [:product_id, :qty] whitelists array of hashes
end
# ── Reading raw JSON body (for HMAC webhook verification) ──────
def webhook
# Must read body BEFORE params is accessed
payload = request.body.read
signature = request.headers["X-Hub-Signature-256"]
unless valid_signature?(payload, signature)
render json: { error: "Invalid signature" }, status: :unauthorized
return
end
# Now parse it
data = JSON.parse(payload)
process_webhook(data)
head :ok
end
def valid_signature?(payload, signature)
expected = "sha256=" + OpenSSL::HMAC.hexdigest(
"SHA256", Rails.application.credentials.webhook_secret, payload
)
ActiveSupport::SecurityUtils.secure_compare(expected, signature.to_s)
end
# ── request.body.read vs params ────────────────────────────────
# request.body is an IO object — calling .read returns the raw string once.
# After params is accessed, Rails may consume the body stream.
# Rule: if you need raw body, read it first and store it.
before_action :cache_raw_body, only: [:webhook]
def cache_raw_body
request.body.rewind
@raw_body = request.body.read
request.body.rewind # allow params to re-parse
endThe key insight is that Rails' param parsing makes JSON APIs transparent to controller code — the same strong parameter patterns used for HTML form submissions protect against mass assignment in JSON APIs. Always use params.require(...).permit(...) and never trust raw params directly. For webhook endpoints that need the raw body for HMAC verification, read request.body.read before any code accesses params.
Rails API Mode: Building JSON-Only Applications
rails new myapp --api generates a stripped-down application stack optimized for JSON APIs. The application inherits from ActionController::API instead of ActionController::Base, removing ~26 middleware layers that are only relevant for HTML applications.
# Generate a new API-only Rails app
rails new myapp --api --database=postgresql
# ── What --api removes ─────────────────────────────────────────
# ActionDispatch::Static (static file serving)
# ActionDispatch::Cookies (cookie jar)
# ActionDispatch::Session::CookieStore
# ActionDispatch::Flash (flash message middleware)
# Rack::MethodOverride (_method param override)
# ActionDispatch::ContentSecurityPolicy::Middleware
# ... and ~20 more view/browser-related middleware layers
# ── app/controllers/application_controller.rb ─────────────────
class ApplicationController < ActionController::API
# ActionController::API includes:
# - Basic rendering (render json:, head)
# - before_action / around_action / after_action
# - Strong parameters
# - Routing
# - Response helpers (render, head)
# NOT included: sessions, cookies, flash, helpers, layouts
# Add JWT authentication globally
before_action :authenticate_request!
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: "Resource not found" }, status: :not_found
end
rescue_from ActionController::ParameterMissing do |e|
render json: { error: e.message }, status: :bad_request
end
rescue_from ActionDispatch::Http::Parameters::ParseError do
render json: { error: "Invalid JSON body" }, status: :bad_request
end
private
def authenticate_request!
token = request.headers["Authorization"]&.split(" ")&.last
@current_user = JwtService.decode(token)
rescue JWT::DecodeError
render json: { error: "Unauthorized" }, status: :unauthorized
end
end
# ── Enabling CORS for API mode ─────────────────────────────────
# Gemfile
gem "rack-cors"
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "https://myapp.com", "http://localhost:3000"
resource "*",
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
expose: ["Authorization"]
end
end
# ── Routes for a RESTful JSON API ──────────────────────────────
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users, only: [:index, :show, :create, :update, :destroy]
resources :posts, only: [:index, :show, :create, :update, :destroy] do
resources :comments, only: [:index, :create]
end
resource :session, only: [:create, :destroy] # login/logout
end
end
get "/health", to: "health#show"
endAPI mode is the right choice when Rails is the backend for a React, Vue, Next.js, or mobile frontend. When the same Rails app also serves HTML (a hybrid app), use the full stack and add ActionController::API behavior selectively with modules. In API mode, session-dependent gems like Devise require additional configuration — use devise-jwt or implement token authentication manually with a JwtService as shown above.
Jbuilder Templates for Complex JSON Responses
Jbuilder ships in the default Rails Gemfile and lets you define JSON structure in .json.jbuilder view files. It is rendered automatically when a controller action responds to format.json without an explicit render json: call. Jbuilder excels at complex nested responses, partial reuse, and fragment caching.
# ── app/views/api/v1/posts/show.json.jbuilder ─────────────────
json.extract! @post, :id, :title, :body, :published_at
# → "id":5,"title":"Hello","body":"...","published_at":"..."
json.url api_v1_post_url(@post)
# → "url":"https://api.example.com/api/v1/posts/5"
json.author do
json.extract! @post.author, :id, :name
json.avatar_url @post.author.avatar_url
end
# → "author":{"id":1,"name":"Alice","avatar_url":"https://..."}
json.tags @post.tags do |tag|
json.extract! tag, :id, :name
end
# → "tags":[{"id":1,"name":"ruby"},{"id":2,"name":"rails"}]
json.stats do
json.view_count @post.view_count
json.comment_count @post.comments.count
end
# ── app/views/api/v1/posts/index.json.jbuilder ────────────────
json.posts @posts do |post|
json.partial! "api/v1/posts/post", post: post
end
json.meta do
json.total_count @posts.total_count
json.current_page @posts.current_page
json.total_pages @posts.total_pages
end
# ── app/views/api/v1/posts/_post.json.jbuilder ────────────────
# Shared partial reused by index, show, related endpoints
json.extract! post, :id, :title, :slug, :published_at
json.author_name post.author.name
json.url api_v1_post_url(post)
# ── Fragment caching in Jbuilder ───────────────────────────────
json.cache! ["v1", @post], expires_in: 5.minutes do
json.extract! @post, :id, :title, :body
json.author do
json.extract! @post.author, :id, :name
end
end
# ── Controller — Jbuilder renders automatically ────────────────
class Api::V1::PostsController < ApplicationController
def show
@post = Post.includes(:author, :tags).find(params[:id])
# No render call needed — Rails renders show.json.jbuilder
# when Accept: application/json or format.json
end
def index
@posts = Post.published.page(params[:page]).per(20)
# Renders index.json.jbuilder
end
end
# ── Using Jbuilder for custom shapes ──────────────────────────
# You can also render Jbuilder templates explicitly:
render "api/v1/posts/show"
render template: "api/v1/shared/error", locals: { message: "Not found" }Jbuilder's json.cache! integrates with Rails' cache store (Redis, Memcached) for fragment-level caching of expensive JSON nodes. When the cache key expires or the underlying record changes, only that fragment is regenerated. For APIs returning deeply nested objects — posts with authors, tags, comments, and comment authors — Jbuilder partials are significantly easier to maintain than a single large render json: call with many include: nesting levels.
Active Model Serializers for Reusable JSON Presentation
Active Model Serializers (AMS) separates JSON presentation from model logic by defining serializer classes alongside models. While the AMS gem (active_model_serializers) has historically been popular, the ecosystem has largely moved toward jsonapi-serializer (formerly fast_jsonapi) for performance-critical APIs and Jbuilder for simpler cases.
# Gemfile
gem "active_model_serializers"
# or for JSON:API format + higher performance:
gem "jsonapi-serializer"
# ── Active Model Serializers ───────────────────────────────────
# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email, :created_at
# Virtual attribute — computed, not a DB column
attribute :avatar_url do
object.avatar.url
end
# Association
has_many :posts, serializer: PostSummarySerializer
belongs_to :organization
end
# app/serializers/post_summary_serializer.rb
class PostSummarySerializer < ActiveModel::Serializer
attributes :id, :title, :published_at
end
# Controller — AMS renders automatically when a serializer exists
class Api::V1::UsersController < ApplicationController
def show
@user = User.find(params[:id])
render json: @user
# Rails finds UserSerializer automatically → uses it
# No manual serializer instantiation needed
end
def index
@users = User.all
render json: @users
# Renders each user through UserSerializer
end
# Explicit serializer selection
def public_profile
@user = User.find(params[:id])
render json: @user, serializer: PublicUserSerializer
end
end
# ── jsonapi-serializer (faster, JSON:API format) ───────────────
# app/serializers/user_serializer.rb
class UserSerializer
include JSONAPI::Serializer
attributes :name, :email, :created_at
attribute :avatar_url do |user|
user.avatar.url
end
has_many :posts
belongs_to :organization
end
# Controller
def show
@user = User.find(params[:id])
render json: UserSerializer.new(@user).serializable_hash
end
def index
@users = User.all
render json: UserSerializer.new(@users).serializable_hash
# JSON:API envelope: { data: [...], included: [...] }
end
# ── Choosing between approaches ────────────────────────────────
# render json: with only: → simplest, no extra gems, 1-2 fields to hide
# Jbuilder → complex nesting, fragment caching, partial reuse
# Active Model Serializers → many models share a JSON shape, Rails convention
# jsonapi-serializer → JSON:API compliance required, high throughputThe jsonapi-serializer gem benchmarks 25x faster than AMS for large collections because it avoids object allocation per record and uses a single-pass serialization loop. For APIs returning lists of 100+ records per request, the performance difference is measurable. For small APIs with simple models, AMS or Jbuilder remain practical. The best rule: start with render json: @model, only: [...], graduate to Jbuilder when response shapes become complex, and evaluate jsonapi-serializer when collection serialization becomes a bottleneck.
JSON Error Responses and rescue_from
Consistent JSON error responses are critical for API clients. Define error handling once in ApplicationController using rescue_from so every controller inherits the same error shapes. Rails symbol status names map cleanly to HTTP codes, and rescue_from catches exceptions before they reach the default HTML error pages.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
# ── Centralized error handling ─────────────────────────────
rescue_from ActiveRecord::RecordNotFound do |e|
render json: {
error: "Not found",
detail: e.message,
}, status: :not_found # HTTP 404
end
rescue_from ActiveRecord::RecordInvalid do |e|
render json: {
error: "Validation failed",
errors: e.record.errors.full_messages,
}, status: :unprocessable_entity # HTTP 422
end
rescue_from ActionController::ParameterMissing do |e|
render json: {
error: "Missing parameter",
detail: e.message,
}, status: :bad_request # HTTP 400
end
rescue_from ActionDispatch::Http::Parameters::ParseError do
render json: {
error: "Invalid JSON body",
}, status: :bad_request # HTTP 400
end
rescue_from Pundit::NotAuthorizedError do
render json: {
error: "Forbidden",
}, status: :forbidden # HTTP 403
end
end
# ── Inline error rendering in controllers ──────────────────────
class Api::V1::PostsController < ApplicationController
def create
@post = Post.new(post_params)
if @post.save
render json: @post, only: [:id, :title, :slug], status: :created
else
# Model validation errors → 422
render json: {
errors: @post.errors.full_messages,
fields: @post.errors.messages, # per-field error hash
}, status: :unprocessable_entity
end
end
def update
if @post.update(post_params)
render json: @post, only: [:id, :title, :updated_at]
else
render json: { errors: @post.errors.full_messages },
status: :unprocessable_entity
end
end
end
# ── RFC 7807 Problem Details format ───────────────────────────
# Standard JSON error envelope for machine-readable errors
rescue_from ActiveRecord::RecordNotFound do |e|
render json: {
type: "https://example.com/errors/not-found",
title: "Resource Not Found",
status: 404,
detail: e.message,
instance: request.path,
}, status: :not_found,
content_type: "application/problem+json"
end
# ── Rails status symbol → HTTP code reference ─────────────────
# :ok 200
# :created 201
# :accepted 202
# :no_content 204
# :bad_request 400
# :unauthorized 401
# :forbidden 403
# :not_found 404
# :unprocessable_entity 422
# :too_many_requests 429
# :internal_server_error 500
# :service_unavailable 503Centralizing rescue_from in ApplicationController ensures that all API endpoints return consistent JSON error shapes — avoiding mixed HTML 500 pages and JSON responses that break client error handling. For ActiveRecord::RecordInvalid, e.record.errors.full_messages returns an array of human-readable strings like ["Name can't be blank", "Email is invalid"], while e.record.errors.messages returns a hash keyed by field name — useful for mapping errors back to form fields in frontend applications. The RFC 7807 application/problem+json content type is the emerging standard for structured error responses in REST APIs.
Key Terms
- render json:
- The Rails controller method that serializes a Ruby object to a JSON string, sets the
Content-Type: application/jsonresponse header, and sends the response body. It acceptsonly:,except:,methods:,include:, andstatus:options. Internally callsobject.as_jsonthenJSON.generate. Available in bothActionController::Base(full stack) andActionController::API(API mode). Returns HTTP 200 by default unlessstatus:is specified. - strong parameters
- The Rails mechanism that prevents mass-assignment vulnerabilities by requiring explicit whitelisting of permitted parameters via
params.require(...).permit(...). In JSON APIs, strong parameters filter the auto-parsed JSON request body the same way they filter HTML form data — the controller code is content-type agnostic. Unpermitted parameters are silently dropped by default; settingconfig.action_controller.action_on_unpermitted_parameters = :raiseraises an error instead, useful for debugging. - ActionController::API
- The slimmed-down base class used by Rails API mode (
rails new --api). It includes only the modules needed for JSON API responses — routing, rendering, strong parameters, and callbacks — and omits session, cookie, flash, view helper, and browser-caching middleware. Inheriting fromActionController::APIinstead ofActionController::Baseremoves approximately 26 middleware layers, reducing memory usage and per-request processing time. All controllers in an API-mode application inherit from it viaApplicationController < ActionController::API. - Jbuilder
- A Rails gem (included in the default Gemfile) that provides a Ruby DSL for building JSON in
.json.jbuilderview files. Jbuilder templates live inapp/viewsalongside HTML templates and follow the same naming conventions —posts/show.json.jbuilderrenders forGET /posts/:idwithAccept: application/json. It supports partials (json.partial!), fragment caching (json.cache!), conditional logic, and nested blocks. The DSL separates JSON presentation from controller logic, making it easy to share response structures via partials. - rescue_from
- An
ActionControllerclass method that registers a handler for a specific exception class. When any action in the controller (or its subclasses) raises the registered exception, Rails calls the handler instead of propagating the error. In API applications,rescue_frominApplicationControllercentralizes JSON error responses for common exceptions likeActiveRecord::RecordNotFound(404),ActiveRecord::RecordInvalid(422), andActionController::ParameterMissing(400). More specific rescue handlers take precedence over more general ones. - as_json
- The Ruby method called by Rails to convert an object to a hash before JSON serialization. ActiveRecord models, arrays, hashes, strings, numbers, and booleans all implement
as_json. You can overrideas_jsonon a model to customize its default JSON representation — useful when a model is serialized in many places and always needs the same field set. Theonly:,except:,methods:, andinclude:options passed torender json:are forwarded toas_json.to_jsoncallsas_jsonthen serializes the result to a JSON string.
FAQ
How do I return a JSON response in Rails?
Call render json: followed by any Ruby object in a controller action. render json: @user serializes an ActiveRecord model; render json: {'{ id: 1, name: "Alice" }'} serializes a plain Hash; render json: @users serializes an array into a JSON array. Rails sets Content-Type: application/json automatically and returns HTTP 200 unless you specify status: — use status: :created (201) for POST success, status: :no_content (204) for DELETE (or use head :no_content for no body). Never use render json: nil — it serializes to the literal string "null" which confuses most API clients. The render json: method is available in both the full stack (ActionController::Base) and API mode (ActionController::API).
How do I filter which fields appear in a Rails JSON response?
Pass only: or except: to render json:. render json: @user, only: [:id, :name, :email] whitelists exactly 3 fields and silently drops every other attribute including password_digest, created_at, and any future columns added to the users table. render json: @user, except: [:password_digest] blacklists specific sensitive fields but exposes any new column added later — making only: the safer default. For virtual attributes (Ruby methods, not database columns), add methods: [:full_name, :avatar_url]. For nested associations: render json: @post, only: [:id, :title], include: {'{ author: { only: [:id, :name] } }'}. To avoid repeating only: across many controller actions, define an as_api_json method on the model or use a Jbuilder template.
How do I read a JSON request body in Rails?
Rails automatically parses application/json request bodies into the params hash. A JSON body like {"user":{"name":"Alice","email":"alice@example.com"}} makes params[:user][:name] return "Alice" — identical to form-encoded input. Use strong parameters as normal: params.require(:user).permit(:name, :email). For nested JSON arrays, use permit(items: [:product_id, :quantity]). For raw JSON bodies (HMAC webhook verification), read request.body.read before params is accessed — Rails may consume the IO stream during param parsing. Call request.body.rewind afterward to allow normal param parsing to continue. If a client sends JSON without a root key, Rails 5.1+ wraps top-level keys under a controller-derived namespace automatically.
What is Rails API mode and when should I use it?
Rails API mode (rails new myapp --api) generates a slimmed-down application that inherits from ActionController::API instead of ActionController::Base, removing approximately 26 middleware layers for HTML rendering — including cookie handling, flash messages, browser caching, and view rendering. Use API mode when your Rails app will never serve HTML and will only be consumed by a separate frontend (React, Vue, mobile). Keep the full stack when the same application renders ERB or Haml views alongside JSON API endpoints. Switching after creation is possible but tedious — decide at rails new time. Gems like Devise, Doorkeeper, and jwt work in API mode; session-dependent features need alternatives like devise-jwt. CORS configuration (via rack-cors) is necessary in API mode since browsers will make cross-origin requests from the separate frontend.
What is Jbuilder and how does it differ from render json:?
Jbuilder is a DSL for building JSON in .json.jbuilder view files that ships in the default Rails Gemfile. It renders automatically when a controller action responds to the JSON format without an explicit render json:. The key difference from render json: is separation of concerns: Jbuilder templates live in the view layer and can be reused via partials (json.partial!) and cached via fragment caching (json.cache!). json.extract! @post, :id, :title extracts named fields; json.posts @posts, :id, :title renders a JSON array. Jbuilder is the better choice when response shapes are complex (5+ associated objects, conditional fields, nested partials) and when you want fragment caching. render json: with only: is faster for simple flat responses and requires no view files.
How do I return a JSON error response in Rails?
Use render json: {'{ error: "message" }'}, status: :unprocessable_entity for validation failures (HTTP 422) and render json: {'{ error: "Not found" }'}, status: :not_found for missing resources (HTTP 404). For model validation errors, use render json: {'{ errors: @user.errors.full_messages }'}, status: :unprocessable_entity — full_messages returns an array like ["Name can't be blank", "Email is invalid"]. Centralize error handling in ApplicationController with rescue_from: rescue_from ActiveRecord::RecordNotFound do render json: {'{ error: "Not found" }'}, status: :not_found end. Rails status symbols map to HTTP codes: :created (201), :bad_request (400), :unauthorized (401), :forbidden (403), :not_found (404), :unprocessable_entity (422). The RFC 7807 application/problem+json format with type, title, status, and detail fields is the emerging standard for machine-readable API errors.
How do I add pagination to JSON responses in Rails?
The kaminari and pagy gems are the most common choices. With kaminari: @users = User.page(params[:page]).per(20) then render json: {'{ users: @users, meta: { current_page: @users.current_page, total_pages: @users.total_pages, total_count: @users.total_count } }'}. With pagy (faster, fewer database queries): include Pagy::Backend in ApplicationController, then @pagy, @users = pagy(User.all, items: 20) and render json: {'{ users: @users, meta: pagy_metadata(@pagy) }'}. Both return total_count, current_page, and total_pages in a meta envelope — the standard pagination pattern for JSON APIs. Always validate the per_page parameter: limit = [[params[:per_page].to_i, 1].max, 100].min caps it between 1 and 100 to prevent clients from requesting thousands of records.
How do I handle JSON parse errors in Rails?
Rails raises ActionDispatch::Http::Parameters::ParseError (Rails 6+) when a request has Content-Type: application/json but a malformed JSON body. Add rescue_from ActionDispatch::Http::Parameters::ParseError in ApplicationController to return a clean 400 JSON response: rescue_from ActionDispatch::Http::Parameters::ParseError do render json: {'{ error: "Invalid JSON body" }'}, status: :bad_request end. Without this, Rails returns an HTML 400 error page, which breaks JSON API clients. In Rails 5.x, the exception class is ActionDispatch::ParamsParser::ParseError. For manual JSON.parse calls (reading request.body.read), wrap in begin/rescue JSON::ParserError. In RSpec tests, verify the handler works by sending a POST with Content-Type: "application/json" and raw_post: "not valid json" and asserting HTTP 400.
Validate your Rails API JSON automatically
Paste a JSON response from your Rails API into Jsonic's validator to check structure, types, and formatting instantly.
Open JSON ValidatorFurther reading and primary sources
- Rails Guides: Action Controller Overview — Official Rails guide covering render json:, strong parameters, filters, and routing
- Rails API Documentation: render — Complete API reference for the render method including json:, status:, and option keys
- Jbuilder GitHub Repository — Jbuilder source, DSL reference, partial examples, and fragment caching documentation
- Active Model Serializers Documentation — AMS gem documentation: serializer classes, associations, and adapter configuration
- jsonapi-serializer: Fast Rails JSON Serializer — High-performance JSON:API serializer for Rails — benchmarks and usage guide