Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool
Ruby on Rails JSON Formatting Best Practices
Recommended Rails Baseline
For modern Rails APIs, the hardest part is usually not generating JSON. It is keeping the contract stable as controllers, models, and clients evolve. A good default in Rails 7 and Rails 8 is to shape responses with an explicit hash or serializer, render through render json:, standardize keys and timestamps, and keep nested data shallow unless the client explicitly needs more.
- Use one response shape consistently across the whole API.
- Let controllers render JSON, but keep formatting rules out of model classes where possible.
- Prefer compact production payloads; pretty-print JSON for debugging, docs, or tests only.
- Serialize only fields you intend to expose instead of dumping full Active Record objects.
Best Practice: Decide your JSON contract first, then make Rails implement that contract consistently.
Use Rails Rendering the Right Way
In current Rails, render json: handles JSON encoding for you. That means you usually should not call to_json manually in the controller. The useful distinction is this:
as_jsonreturns a Ruby hash/array structure that Rails can still compose, transform, or wrap.to_jsonreturns an encoded JSON string, which is usually too late in the pipeline for clean controller logic.
Controller example:
class Api::V1::UsersController < ApplicationController
def show
user = User.find(params[:id])
render json: {
data: {
id: user.id.to_s,
type: "user",
attributes: {
email: user.email,
created_at: user.created_at.iso8601(3)
}
}
}
end
endas_json vs to_json:
user.as_json(only: %i[id email])
# => { "id" => 1, "email" => "a@example.com" }
user.to_json
# => "{\"id\":1,\"email\":\"a@example.com\"}"Best Practice: In controllers, prefer render json: some_hash_or_serializer_output instead of render json: model.to_json.
Pick One Key Convention and Enforce It
Rails applications naturally use snake_case for Ruby methods, params, and database columns. Many JavaScript clients prefer camelCase. Either choice is defensible. The mistake is mixing both across endpoints.
When snake_case is the better default
Keep snake_case if your API is mostly consumed by Rails or backend services, or if you want the simplest mapping from serializers to model attributes.
Snake case response:
{
"user_id": "42",
"first_name": "Alice",
"last_name": "Smith",
"email_verified": true
}When camelCase is worth it
Use camelCase when your contract is primarily for web or mobile frontend consumers and you want the client to receive keys in its native style. Do the transform in your serializer or JSON builder, not by renaming database columns or Ruby methods.
Jbuilder and serializer options:
# Jbuilder json.key_format! camelize: :lower json.deep_format_keys! # JSONAPI::Serializer class UserSerializer include JSONAPI::Serializer set_key_transform :camel_lower end
Best Practice: Choose one key style for the API surface and convert at the presentation layer.
Format Types Deliberately
JSON bugs often come from type drift, not indentation. Rails already helps here, but your contract still needs explicit rules for timestamps, nulls, booleans, money, and identifiers.
- Timestamps: Keep them as ISO 8601 strings. Rails uses standard JSON time formatting by default, and it is worth preserving that consistency across the API.
- Nulls: Use
nullwhen a field exists but currently has no value. Omit a field only when it is intentionally unavailable, permission-gated, or not requested. - Booleans: Return real
trueandfalse, never0,1, or string equivalents. - IDs: If JavaScript clients may consume the API and identifiers can exceed the safe integer range, send IDs as strings.
- Money and exact decimals: Prefer an integer minor unit such as cents, or a decimal string, rather than relying on floating-point assumptions in the client.
Example payload:
{
"id": "9007199254740993",
"starts_at": "2026-03-11T09:30:00.123Z",
"cancelled_at": null,
"price_cents": 1999,
"paid": true
}Best Practice: Make every field's type stable enough that clients never have to guess or branch on it.
Keep Associations Predictable and Cheap
Deeply nested Rails JSON is attractive at first because it reduces client requests. It also creates larger payloads, hides N+1 queries, and makes response contracts harder to evolve. Most APIs age better when the default response is shallow and expanded relationships are opt-in.
A practical rule
- Embed small, always-needed child data only when it is truly part of the primary resource.
- Use IDs or relationship objects for larger associations.
- If you support
includeor similar expansion params, eager load the same relationships in the query layer.
Safer pattern:
posts = Post.includes(:author).order(created_at: :desc)
render json: {
data: posts.map { |post|
{
id: post.id.to_s,
title: post.title,
author: {
id: post.author.id.to_s,
name: post.author.name
}
}
}
}Best Practice: Never serialize associations you did not preload, and do not let convenience turn into N+1 queries.
Wrap Lists with Metadata and Links
Collection endpoints should tell clients more than just the current page of records. A stable list envelope makes pagination, caching, and debugging easier.
Collection response:
{
"data": [
{ "id": "1", "name": "Item 1" },
{ "id": "2", "name": "Item 2" }
],
"meta": {
"current_page": 1,
"per_page": 25,
"total_pages": 10,
"total_count": 250
},
"links": {
"self": "/api/v1/items?page=1",
"next": "/api/v1/items?page=2",
"prev": null
}
}If you already follow JSON:API or another external spec, keep that envelope everywhere instead of creating one style for list endpoints and another for detail endpoints.
Return Machine-Friendly Errors
Clients should not need to parse free-form English to know what went wrong. Good Rails JSON errors include a stable code, a human-readable message, the HTTP status, and enough context to highlight the failing field or support the request.
Error response:
{
"errors": [
{
"status": "422",
"code": "invalid_email",
"detail": "Email is not a valid address",
"source": { "pointer": "/data/attributes/email" },
"request_id": "f5c4ce20-65f1-4e8a-8b6d-8d2bc62b3df8"
}
]
}Best Practice: Keep the top-level errors shape identical for validation failures, auth failures, and business-rule failures.
Choose the Smallest Tool That Keeps You Honest
Rails does not force one JSON formatting approach. Pick the smallest one that still keeps response shaping explicit and testable.
- Plain hashes with
render json:: best for tiny internal endpoints or one-off actions. - Jbuilder: good when you want a view-like DSL and built-in key formatting controls.
- JSONAPI::Serializer: a strong fit when you want JSON:API documents, relationship handling, sparse fieldsets, and key transforms in serializer classes.
What matters more than the gem choice is the boundary: controllers should coordinate, serializers or builders should format, and models should focus on domain logic instead of presentation.
Common Rails JSON Mistakes
- Calling
render json: record.to_jsonwhenrender json: recordor a hash is cleaner. - Exposing full model attributes and associations by default.
- Returning one endpoint in
snake_caseand the next incamelCase. - Pretty-printing production responses and paying the bandwidth cost for no client benefit.
- Letting JSON routes fall back to HTML error pages or framework-default exception pages.
- Serializing nested records without matching
includesorpreloadcalls.
Bottom Line
The best Ruby on Rails JSON formatting strategy is boring in the right way: explicit, documented, and consistent. Use render json: as the boundary, keep key and type rules stable, preload what you serialize, and make error payloads as predictable as success payloads. That gives frontend and API consumers a contract they can trust as your Rails app grows.
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool