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_json returns a Ruby hash/array structure that Rails can still compose, transform, or wrap.
  • to_json returns 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
end

as_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 null when 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 true and false, never 0, 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 include or 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_json when render json: record or a hash is cleaner.
  • Exposing full model attributes and associations by default.
  • Returning one endpoint in snake_case and the next in camelCase.
  • 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 includes or preload calls.

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