Update: Edge Rails now does whatever Jsonifier does (and more). Check out my blog post on JSON serialization maturity in edge Rails. This means that this plugin is virtually obsolete if you're using edge Rails.

If you've tried to output JSON from your Rails applications before, you'd probably have noticed how inadequate it all seems. Let's look at the kind of output (pretty-printed for easier reading) you get from calling to_json on your typical User model:

{
  attributes: {
    id: 1,
    name: "Konata",
    awesome: true,
    created_at: "07/01/2007"
  }
}

Some of the common problems developers who write applications that speak JSON have with this are:

  • The attributes part is often unnecessary cruft when consuming ActiveRecord objects as JSON.
  • There's no obvious way to control which attributes are shown in the JSON (e.g. you may want to hide private or lengthy attributes). The way to workaround this is either to write your own to_json instance method for your ActiveRecord model, or to use a Rails plugin (like acts_as_json) or a gem.
  • Including 2nd (or higher order) associations is tricky (as above, you can achieve this by defining your own to_json instance methods).

Under the hood of the current ActiveRecord#to_json

The to_json method for ActiveRecord objects falls back on using that defined in ActiveSupport's JSON classes. The way it's currently done, there are a bunch of JSON encoder classes defined for various types - when you call to_json on an ActiveRecord object it simply uses Object#to_json.

By the way, see that created_at: "07/01/2007" bit in the example above which is the JSON output for a Date attribute? That's in MM/DD/YYYY format. Yup, 1st July 2007 not 7th January 2007 you "rest of the world you". Ouch, but at least you can use JavaScript's Date.parse() on it directly as detailed in this patch. And before you ask, yes, Date.parse() doesn't understand the YYYY-MM-DD format.

Can has Rails patch?

So about 2 days ago, I wrote a Rails patch to boost ActiveRecord#to_json with an options hash. Yep much like what you can do with ActiveRecord#to_xml.

I hope it gets committed into the core some day. I mean, gosh, isn't Rails all about Web 2.0 mashups. It even has that fancy render :json thing. I do believe JSON output needs to be treated more seriously in Rails rather than the half-hearted attempt at JSON encoding. No? Well, if the patch doesn't get accepted I'll still have my plugin (see below).

Anyway, let's see some examples:

Some examples, see?

Assuming User and Post models where User has_many Posts:

david = User.find(1)
david.to_json  # {id: 1, name: "David", awesome: true, created_at: "07/01/2007"}

No more attributes cruft!

:only and :except work the same way that as for ActiveRecord#to_xml:

david.to_json(:only => :name)                 # {name: "David"}
david.to_json(:only => [:id, :name])          # {id: 1, name: "David"}
david.to_json(:except => :created_at)         # {id: 1, name: "David", awesome: true}
david.to_json(:except => [:id, :created_at])  # {name: "David", awesome: true}

You can use the :methods options as well to include any methods on the object.

david.to_json(:methods => :permalink)
  # {id: 1, name: "David", awesome: true, created_at: "07/01/2007", permalink => "1-David"}
david.to_json(:methods => [:permalink, :interestingness])
  # {id: 1, name: "David", awesome: true, created_at: "07/01/2007", \
  #   permalink => "1-David", :interestingness => 666}

The :include option lets you include associations.

david.to_json(:include => :posts)
  # {id: 1, name: "David", awesome: true, created_at: "07/01/2007", \
  #    posts: [{id: 1, author_id: 1, title: "Welcome to the weblog"}, \
  #            {id: 2, author_id: 1, title: "So I was thinking"}]}

:only, :except, and :methods works on the included associations as well:

david.to_json(:include => { :posts => { :only => :title } })
  # {id: 1, name: "David", awesome: true, created_at: "07/01/2007", \
  #    posts: [{title: "Welcome to the weblog"}, \
  #            {title: "So I was thinking"}]}

Of course, 2nd level (and higher order) associations work too:

david.to_json(:include => { :posts => { \
                              :include => { :comments => { \
                                              :only => :body } }, \
                              :only => :title } })
  # {id: 1, name: "David", awesome: true, created_at: "07/01/2007", \
  #    posts: [{comments: [{body: "1st post!"}, {body: "OMGWTFBBQ!"}], \
  #             title: "Welcome to the weblog"}, \
  #            {comments: [{body: "Don't think too hard"}], \
  #             title: "So I was thinking"}]}

Please do give any feedback at the Rails patch for ActiveRecord#to_json or here. I wanna know what you feel about adding this functionality into the Rails core!

Jsonifier plugin

I've rolled the patch into a plugin: Jsonifier. The Jsonifier Trac is at http://trac.codefront.net/jsonifier/.

Git repository:

git://github.com/chuyeow/jsonifier.git

Github project page:

http://github.com/chuyeow/jsonifier/

To install the plugin:

ruby script/plugin install git://github.com/chuyeow/jsonifier.git

or git clone the repository manually into the vendor/plugins directory.

A note on valid JSON

The JSON Rails spits out by default is not strictly valid JSON since the JSON specifications require keys to be double quoted. To get strictly valid JSON, add

ActiveSupport::JSON.unquote_hash_key_identifiers = false

in your environment.rb (or in the Rails initializers directory if you're on edge). See my blog post on how to get strictly valid JSON from Rails for more info.