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.

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
endThe 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
endThe _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
endTo understand this method, we need two pieces of information:
- How renderers are initialized in the
_renderersvariable. - How
method_nameis 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
endThe add method takes the name of the renderer (:json) and a block, and
- Dynamically defines a method that will execute the block later.
- 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
endFinally, 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}"
endPassing :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
endAnd 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
endAs 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)
endIt 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
endThe 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.

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

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
endThis 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
endThe 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:

# 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
endWhich ultimately returns the following JSON you see in the browser.

Conclusion
Whenever we invoke the following method in our application
render json: @postit invokes the block associated with the :json renderer.
add :json do |json, options|
json.to_json(options) # simplified version
endThe 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.
