ruby 81 lines · 3 tabs

Building Nested JSON Presenters in Rails Without ActiveModel::Serializer

Shared by codesnips Jul 2026
3 tabs
class ProjectPresenter
  def initialize(project, include_tasks: true)
    @project = project
    @include_tasks = include_tasks
  end

  def as_json(*)
    payload = {
      id: @project.id,
      name: @project.name,
      archived: @project.archived?,
      owner: UserPresenter.new(@project.owner).as_json,
      task_count: @project.tasks.size,
      updated_at: @project.updated_at.iso8601
    }
    payload[:tasks] = presented_tasks if @include_tasks
    payload
  end

  private

  def presented_tasks
    @project.tasks.map { |task| TaskPresenter.new(task).as_json }
  end
end
3 files · ruby Explain with highlit

This snippet shows how a Rails API endpoint can serialize a nested resource graph using plain Ruby presenter objects instead of reaching for a heavier serialization gem. The pattern keeps view logic out of both the model and the controller, so the model stays focused on persistence and business rules while the presenter owns the shape of the JSON payload.

In ProjectPresenter, the object wraps a single Project and exposes an as_json method that returns a plain hash. The important design choice is that nested associations are delegated to their own presenters: tasks maps each record through TaskPresenter, and owner is wrapped in a UserPresenter. This keeps each presenter responsible for exactly one resource type, so the composition mirrors the object graph. The include_tasks: keyword lets a caller opt out of the expensive nested collection, which is how a list endpoint can stay lightweight while a detail endpoint returns everything.

TaskPresenter demonstrates why presenters are more than a hash literal: due_label derives a human string from due_on, and status normalizes a raw enum-ish column into an API-friendly value. These are presentation concerns that would pollute the model if placed there, and would be duplicated across actions if placed in the controller. Because each presenter is a normal Ruby object, methods like overdue? are trivial to unit test in isolation without touching the database or the HTTP layer.

In ProjectsController, the actions stay thin: index builds a collection with includes(:owner, :tasks) to avoid N+1 queries, then maps each record through a presenter with include_tasks: false. The show action returns the full nested payload. Passing the include_tasks flag down is what makes the same presenter reusable across both actions.

The main trade-off is that presenters are manual — there is no automatic attribute reflection, so new fields must be added by hand. In exchange the payload is explicit, versioning is easy, and eager loading is controlled at the call site rather than hidden inside a serializer DSL. This approach fits teams that want predictable JSON and readable, testable view objects over configuration-driven magic.


Related snips

Share this code

Here's the card — post it anywhere.

Building Nested JSON Presenters in Rails Without ActiveModel::Serializer — share card
Link copied