Rendering JSON from Rails controllers
Fast recall
Quick revision
Core answer
Whether the learner treats JSON as a public representation rather than a dump of an Active Record object.
Rendering every model column by default or calling render and then continuing into a second response.
Discuss status, field selection, associations, error shape, and serialization boundaries.
Interview-ready guide
Interview answer
In a Rails controller, render json: turns the supplied Ruby value into the response body and uses the JSON format. I pass an explicit representation—often a hash, serializer result, or carefully selected model fields—rather than assuming every database column belongs in a public API.
The response also needs an honest status. A normal read can use the default 200, creation can use :created, a validation failure can use 422, and an operation that intentionally returns no representation can use head :no_content. A 204 response should not include JSON.
For example, an article endpoint can return { id, title, author: { id, name } } and omit internal moderation flags, password-related fields, and timestamps the contract does not promise. An error can return a stable list of field and code values instead of an exception message or HTML page.
render does not automatically stop Ruby method execution. The action should use an if/else branch, return, or another clear control flow so it does not later render or redirect again. Associations also need query planning; serializing comments one record at a time can create an N+1 query problem.
I keep serialization separate enough to test its shape, version changes deliberately, and use request tests for content type, status, and exact keys. JSON is a representation of a resource for clients, not a mirror of the current model schema.
Rendering JSON from Rails controllers
Direct Answer
Use render json: with an explicit representation and status, selecting only the fields the API contract exposes. Keep success and error shapes consistent, avoid leaking whole models or internal exceptions, and use head for a response that intentionally has no body.
Deep Dive & Architecture
Start with the client-visible resource contract. Decide which identifiers, attributes, relationships, links, and error codes a caller needs. This prevents accidental exposure of columns such as password digests, internal notes, deletion flags, or operational counters. A serializer library can organize that contract, but installing one does not make the field choices automatically correct.
Rails can serialize a hash, array, or object passed to render json:. For small endpoints, constructing a hash can be explicit and easy to review. For repeated or nested representations, a serializer object or view layer avoids duplication. Whichever tool is used, keep the shape deterministic: a collection should not switch between an array and an object, and an absent optional field should follow a documented null or omission rule.
Errors deserve the same care. A model's human-readable full messages are useful for display, but clients often also need stable field and machine code values. Authentication errors should not reveal whether an email exists. Unexpected exceptions should be logged with a request correlation identifier and mapped to a generic server response rather than returning stack traces.
Serialization can trigger database work. Rendering a hundred articles and asking each one for its author may issue a query per row. Load required relationships with an appropriate Active Record query and verify the SQL count. Avoid including huge nested graphs by default; pagination and links usually create a clearer boundary than recursively embedding every association.
Finally, response control flow matters. Rails allows one response per request. render sets the response but does not halt the method, so a later render can raise a double-render error or later code can perform unwanted work. Branch once, return where clarity requires it, and cover successful, empty, invalid, unauthorized, and missing cases with request tests.
A contract test can serialize a representative record and compare only public keys, types, and nesting. That catches accidental exposure when a model gains a column and catches silent breaking changes when a serializer renames or removes a field.
Code Example
class Api::ArticlesController < ActionController::API
def show
article = Article.includes(:author).find(params[:id])
render json: {
id: article.id,
title: article.title,
author: { id: article.author.id, name: article.author.name }
}, status: :ok
end
end
The endpoint exposes a deliberate shape and preloads the one relationship it serializes.
Common Mistakes & Pitfalls
Rendering every model column by default or calling render and then continuing into a second response.
Check your understanding
Answer these without looking back at the article.
- Why can serializing associations cause N+1 queries?
- When would you use head :no_content instead of render json:?
- How would you version a breaking JSON field change?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.