Migrating to Rails 3.0 Gotchas: as_json bug

Since Rails 2.3.3, the best practice to take control over what attributes are exposed during a JSON serialization was to override the as_json method as opposed to the to_json method as it was done previously. The idea is that as_json should return the object structure of the JSON (most likely a Hash) and to_json encode that object into a JSON string. Jonathan Julian wrote a good reference post on that subject.

The as_json method takes an options hash that you can use to include/exclude certain attributes.

So let say you have a Product model with id, name, created_at, updated_at and account_id as attributes. You don't want to expose the relationship to account and only include id, name and created_at. You would override as_json in your model as follows:

def as_json(options=nil)
  super( {:only => ["id", "name", "created_at"]} )
end

You can verify that it works in the Rails console:

irb(main):003:0> Product.create(:name=>"Test").as_json
=> {"name"=>"Test", "created_at"=>Mon, 06 Sep 2010 21:38:29 PDT -07:00, "id"=>2}
irb(main):004:0> Product.create(:name=>"Test").to_json
=> "{\"name\":\"Test\",\"created_at\":\"2010-09-06T21:39:11-07:00\",\"id\":3}"

Unfortunately, Rails 3.0 introduced a bug, where the as_json method in the super class (ActiveRecord::Base) will just return the object and ignore the options passed. As a result in this case to_json will output all the attributes contained in your model.

In the Rails 3.0.0 console, you get:

irb(main):001:0> Product.create(:name=>"Test").as_json
=> #<Product id: 4, name: "Test", account_id: nil, created_at: "2010-09-07 04:40:44", 
   updated_at: "2010-09-07 04:40:44">
irb(main):002:0> Product.create(:name=>"Test").to_json
=> "{\"name\":\"Test\",\"created_at\":\"2010-09-06T21:40:49-07:00\",\"updated_at\":
   \"2010-09-06T21:40:49-07:00\",\"account_id\":null,\"id\":5}"

Whoops, account_id is being included.

As a workaround, you can either hand-craft the attributes your want to return or use the following to simply output a hash:

def as_json(options=nil)
  serializable_hash({:only => ["id", "name", "created_at"] })
end

Note that in these examples, the ActiveRecord::Base.include_root_in_json setting was set to false so that the root object class name is not included.

Talk Back