What Happens When You Call render? Let's Understand the Rails Rendering Process

This article explains the Rails rendering process in the context of returning JSON data from the controller. Hopefully, it will make it clear what really happens when you call the render method from the controller.

7 min read

Hope you had a wonderful Christmas and are looking forward to 2023. Let's wrap this year by understanding the Rails rendering stack... 😉


This post assumes an understanding of metaprogramming concepts in Ruby. If you aren't comfortable with it, an excellent place to start is by reading the book Metaprogramming Ruby 2.

Ruby on Rails makes it very easy to render JSON data from the controller using the render method.

Ever wondered what really goes on behind the scenes? How does the render method convert the list of tasks to JSON format and sends it in the response? How Rails creates the renderers? This article will try to explain how rendering works in Rails.

By the end of the article, you should have a much better understanding of what really happens when you call the render method from the controller.


Let's start our journey into the rendering process by considering the Rails controller hierarchy.

Your typical Rails controllers inherit from the ApplicationController class which itself inherits from the ActionController::Base class. This class includes the AbstractController::Rendering module, providing the render method. Hence the render method becomes available in all your controllers.

Rails Controller Hierarchy
Rails Controller Hierarchy

When you call render from the PostsController, Rails calls the AbstractController::Rendering#render method and sets its result on the response.

class PostsController < ApplicationController
  def show
    @post = Post.first
    render json: @post
  end
end


# actionpack/lib/abstract_controller/rendering.rb

module AbstractController
  module Rendering
    def render(*args, &block)
      options = _normalize_render(*args, &block)
      rendered_body = render_to_body(options)
      self.response_body = rendered_body
    end
  end
end

The normalize_render method creates the hash of options containing the passed data @posts along with the template and the layout. It looks like this:

options = {
  :json=>#<Post:0x00000001145d0470>,
  :template=> ..,
  :layout=> ..
}

Ignore the template and layout keys for now. The important point is that the options[:json] contains the post object we passed in the controller.

Now let's move to the next line in the render method, which calls the render_to_body method, passing the options hash. This method resides in the  action_controller/metal/renderers.rb file.

# action_controller/metal/renderers.rb

module ActionController
  module Renderers
    def render_to_body(options)
      _render_to_body_with_renderer(options) || super
    end
  end
end

The _render_to_body_with_renderer method loops over a set of pre-defined renderers and checks if it includes the renderer we need (:json in our case). If it does, then it dynamically generates and calls a method named method_name using the Object#send method.

# action_controller/metal/renderers.rb

module ActionController
  module Renderers
    def _render_to_body_with_renderer(options)
      _renderers.each do |name|
        if options.key?(name)
          _process_options(options)
          method_name = Renderers._render_with_renderer_method_name(name)
          return send(method_name, options.delete(name), options)
        end
      end
      nil
    end
  end
end

To understand this method, we need two pieces of information:

  1. How renderers are initialized in the _renderers variable.
  2. How method_name is generated and what it does when called.

The _renderers variable

It's a Set containing renderer names that correspond to available renderers. Default renderers are json, js, and xml.

This variable is defined in the Renderers::All concern. As soon as it's included in the Base class, it includes the ActionController::Renderers module. This module sets up the RENDERERS variable to an empty set and then calls the add method which fills this set.

# action_controller/metal/renderers.rb

module ActionController
  module Renderers
    RENDERERS = Set.new
  
    module All
      extend ActiveSupport::Concern
      include Renderers
      
      included do
        self._renderers = RENDERERS
      end
    end
    
    add :json do |json, options|
      json.to_json(options) # simplified
    end
    
    add :xml { ... }
    
    add :js { ... }
  end
end

The add method takes the name of the renderer (:json) and a block, and

  1. Dynamically defines a method that will execute the block later.
  2. Add the name of the renderer to RENDERERS, a set of renderer names.
# action_controller/metal/renderers.rb

module ActionController
  module Renderers
    def self.add(key, &block)
      define_method(_render_with_renderer_method_name(key), &block)
      RENDERERS << key.to_sym
    end
  end
end

Finally, the RENDERERS variable contains this array: [:json, :js, :xml] which gets assigned to the _renderers variable.

This brings us to the next point, that is, how the method named method_name is generated.

How method_name is generated

Let's inspect the above add method in detail. It defines a method dynamically using the Module#define_method and passes it a name which it generates using the _render_with_renderer_method_name helper.

def self._render_with_renderer_method_name(key)
  "_render_with_renderer_#{key}"
end

Passing :json will return the string _render_with_renderer_json. This is the name of the dynamic method. Its body is the block we passed to the add method.

To summarize, calling add :json as above defines the following method:

def _render_with_renderer_json(json, options)
  json.to_json(options)  # simplified
end

And this gives us the final piece of the puzzle: What the method defined by method_name does when called. It simply calls to_json on the object you passed to the render method and returns it.

To generalize, it renders the given object, depending on the renderer.

Let's unwind the stack and go back to where we started, the render_to_body_with_renderer method.

# action_controller/metal/renderers.rb

module ActionController
  module Renderers
    def _render_to_body_with_renderer(options)
      _renderers.each do |name|
        if options.key?(name)
          _process_options(options)
          method_name = Renderers._render_with_renderer_method_name(name)
          return send(method_name, options.delete(name), options)
        end
      end
      nil
    end
  end
end

As we saw earlier, the options hash looks like this:

options = {
  :json=>#<Post:0x00000001145d0470>,
  :template=> ..,
  :layout=> ..
}

Since options contains the :json renderer, the control enters the if conditional block and calls the dynamically generated _render_with_renderer_json method. Additionally, it passes the value corresponding to the :json key, which is an instance of the Post class.

Let's revisit the dynamically generated _render_with_renderer_json method body again.

def _render_with_renderer_json(json, options)
  json.to_json(options)
end

It will call post.to_json and return the following output.

"{\"title\":\"hello world\"}"

Let's unwind the stack once more and return to the render method we saw at the beginning.

# abstract_controller/rendering.rb (simplified)

def render(*args, &block)
  options = _normalize_render(*args, &block)
  rendered_body = render_to_body(options)
      
  self.response_body = rendered_body
end

The above JSON string is assigned to the response_body on the controller, which is ultimately sent to the client.

If you're curious how response_body actually works, keep on reading.

Sending Response

Let's inspect the Rails controller hierarchy once again.

The Rails Controller Hierarchy
The Rails Controller Hierarchy

Ultimately, your PostsController class inherits from the ActionController::Metal class. To learn more about the metal controller, check out the following article.

Metal Controller in Rails
The ActionController::Base is the great-grandfather of all Rails controllers, but doesn’t get much attention. In this post, we will examine this Metal class and how it works.

The ActionController::Metal class provides the setter for the response_body, which assigns the value to the response body.

# action_controller/metal.rb

module ActionController
  class Metal
    def response_body=(body)
      body = [body] unless body.nil? || body.respond_to?(:each)

      if body
        response.body = body
        super
      else
        response.reset_body!
      end
    end
  end
end

This whole request lifecycle was part of the dispatch method. Once the action is processed, the dispatch method calls to_a method, returning the response.to_a result.

# action_controller/metal.rb

module ActionController
  class Metal
    def dispatch(name, request, response)
      set_request!(request)
      set_response!(response)
      process(name)
      request.commit_flash
      to_a
    end
    
    def to_a 
      response.to_a
    end
  end
end

The to_a method on the Response calls the rack_response method, which returns the final result, the Rack-compatible array of the status, headers,  and body. To learn more about Rack, check out the following article:

The Definitive Guide to Rack for Rails Developers
This article explains pretty much everything you need to know about Rack as a Ruby and Rails developer. We will start with the basics and move to more advanced concepts like middleware and the Rack DSL.
# action_dispatch/http/response.rb

module ActionDispatch
  class Response
    def to_a
      rack_response @status, @header.to_hash
    end
    
    def rack_response(status, header)
      [status, header, RackBody.new(self)]
    end
  end
end

Which ultimately returns the following JSON you see in the browser.

Conclusion

Whenever we invoke the following method in our application

render json: @post

it invokes the block associated with the :json renderer.

add :json do |json, options|
  json.to_json(options) # simplified version
end

The local variable json inside the block points to the @post object, and the other options passed to render will be available in the options variable. The block returns the JSON representation of the post and sends it to the client.

And that's what happens when you call render json: @post from your controller.


I hope you found this article useful and that you learned something new.

If you have any questions or feedback, or didn't understand something, please leave a comment below or send me an email. I look forward to hearing from you.

Please subscribe to my blog below if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.