Rails JSON Views

Summary: JSON generation belongs in Rails views, not in models. as_json saves time, but is too fragile. Try the rjson gem or Presenters, instead.

I'm pretty sure JSON rendering belongs in the View part of Model-View-Controller. To me, there's not much of a difference between returning "[1, 2, 3]" and "<li>1</li><li>2</li><li>3</li>". They're just two different ways of presenting an array.

There are certain methods commonly placed in ActiveRecord models that I feel the same way about, like #full_name on a Person model. I wouldn't put #full_name_with_last_name_bold in a model, but that method would contain a lot of the same logic as #full_name. They're both data formatters. I don't think methods like #full_name end up in a model because they belong there; presentation methods wind up in the model because it's convenient.

I think that's why #as_json is in ActiveModel: that's a convenient place. But look at this example from the Tequila README of what controller code could look like for rendering an object as JSON:

@humans.to_json(
  :methods => [:login, :enhanced_name, :hello_world], :except => [:created_at, :updated_at],
  :include => {:pets => { :except => [:human_id, :pet_type_id, :created_at, :updated_at],
    :methods => :pet_type_name,
    :include => { :toys => { :methods => [:railsize2], :only => [:color, :size] } }
  }}
)

I admit this looks contrived, but imagine what controllers would look like if #to_json were used to generate responses for Facebook Graph API. The JSON representation of each model depends on both the context and the person viewing the data. Compared to that, the example above seems trivial.

So how can we generate JSON more easily? I think we can agree that erb does a pretty good job for HTML, so we might try straight erb:

[
  <% @forums.each do |forum| %>
  {
    "name" : <%= @forum.name %>,
    "topics" : [
      <% @forum.topics.each do |topic| %>
      {
        "author" : <%= topic.author.full_name %>,
        "title" : <%= topic.title %>,
        "posts" : [
          <% topic.posts.each do |post| %>
          {
            "author" : <%= post.author.full_name %>,
            "body" : <%= post.body %>
          },
          <% end %>
        ]
      },
      <% end %>
    ]
  },
  <% end %>
]

But that already feels complicated and it doesn't even handle commas correctly in the arrays. And it doesn't take advantage of the fact that JSON output is nearly 1:1 with the Ruby objects you want to represent with it. That makes me wonder: what would a Ruby hash version look like?

@forums.map do |forum|
  {
    :name => forum.name,
    :topics => forum.topics.map do |topic|
      {
        :author => topic.author.full_name,
        :title => topic.title,
        :posts => topic.posts.map do |post|
          {
            :author => post.author.full_name,
            :body => post.body
          }
        end
      }
    end
  }
end

It's promising in several ways. You can tell at a glance what JSON will be generated. It doesn't require a page of documentation to explain how you would customize this to show something else. Your JSON won't change beneath your feet when a table changes. (Tequila fails all three of these tests.) And it doesn't seem to suffer from the domain mismatch problems HTML DSLs tend to, which require patches like Markaby's #capture and Haml's precede/succeed/surround. But it's still verbose enough that I wouldn't want to write it. I'd rather ditch the braces:

@forums.map do |forum|
  :name => forum.name,
  :topics => forum.topics.map do |topic|
    :author => topic.author.full_name,
    :title => topic.title,
    :posts => topic.posts.map do |post|
      :author => post.author.full_name,
      :body => post.body
    end
  end
end

Unfortunately, that's no longer Ruby. But that's liberating, in a way. Hmm…

@forums [ |forum|
  :name,
  :topics => forum.topics [ |topic|
    :author => topic.author.full_name,
    :title,
    :posts => topic.posts [ |post|
      :author => post.author.full_name,
      :body
    ]
  ]
]

Anyway, I don't have an answer yet. I am experimenting, though.

rjson

Update: skip ahead to the next section, "Better solutions," if you're more interested in production-ready code than my crappy hacks.

I've created a new Rails template handler, rjson. It's very simple so far: views named *.rjson are evaluated as Ruby then #to_json is called on its return value. It's simple, but it works. Here's the code I use I make the magic happen in Rails 3.0.3:

# lib/rjson.rb

module RJSON
  class Handler
    class_attribute :default_format
    self.default_format = Mime::JSON

    def call(template)
      "begin;#{template.source};end.to_rjson"
    end
  end
end

class Object
  # calls #to_json, but redefines #as_json on the result to
  # return what the object was before the JSON conversion.
  # this way render(:partial => "xxx") effectively returns
  # the object rather than a JSON string (though it is just
  # a string until the container template is rendered).
  def to_rjson
    o = to_json
    o.instance_variable_set(:@pre_json, self)
    def o.as_json(options = {})
      instance_variable_get(:@pre_json)
    end
    o
  end
end
# config/initializers/rjson.rb

require "rjson"
ActionView::Template.register_template_handler(:rjson, RJSON::Handler.new)

Better solutions

I wrote that code a long time ago and by the time I finally cleaned it up to where I felt comfortable releasing it as a gem, somebody beat me to it, but it's okay because they also did a much better job.

I also think that Presenters have a lot of potential.

DHH seems to be interested in jbuilder.

Did I level up with this post?


Comments

Click here to view the comments on this post, or just send me an e-mail.