Rails to_json or as_json?

A really great modification was introduced in Rails 2.3.3 - and while everyone clamored about JSON encoding speeds and C vs Ruby implementations, the blogosphere overlooked the clean separation of responsibility that was introduced.

In the “old days”, you’d override to_json in your model class to provide a JSON implementation of your model. Then in your controller, render :json => @model would work perfectly. And some folks would even redundantly code render :json => @model.to_json, and that would work too.

to_json even had some great options for ActiveRecord objects! You could tell the method to only render certain attributes, or to include associations or method calls!

render :json => 
  @user.to_json(:only => [:email], :include => [:addresses])

Life was good. But things start to fall apart when you want to do something a little out of the ordinary. Like return JSON with the model as part of a bigger structure.

render :json => { :success => true, 
  :user => @user.to_json(:only => [:email]) }

Oops. {\"user\":{\"email\":\"me@example.com\","success":true}has the JSON characters escaped, which is not what we want. So what do we do? We hack around it:

render :json => { :success => true, 
  :user => { :email => @user.email } }

But this doesn’t scale - we have to explicitly create the JSON by hand in the controller. What if we need 5 or more attributes? Yuck!

Enter ActiveSupport 2.3.3. Now the creation of the json is separate from the rendering of the json. as_json is used to create the structure of the JSON as a Hash, and the rendering of that hash into a JSON string is left up to ActiveSupport::json.encode. You should never use to_json to create a representation, only to consume the representation.

def as_json(options={})
  { :email => self.email }
end

Anytime to_json is called on an object, as_json is invoked to create the data structure, and then that hash is encoded as a JSON string using ActiveSupport::json.encode. This happens for all types: Object, Numeric, Date, String, etc (see active_support/json).

ActiveRecord objects behave the same way. There is a default as_json implementation that creates a Hash that includes all the model’s attributes. You should override as_json in your Model to create the JSON structure you want. as_json, just like the old to_json, takes an option hash where you can specify attributes and methods to include declaratively.

def as_json(options={})
  super(:only => [:email, :avatar], :include =>[:addresses])
end

Your controller code to display one model should always look like this:

render :json => @user

And if you have to do anything out of the ordinary, call as_json passing your options.

render :json => { :success => true, 
  :user => @user.as_json(:only => [:email]) }

The moral of the story is: In controllers, do not call to_json directly, allow render to do that for you. If you need to tweak the JSON output, override as_json in your model, or call as_json directly.

Fix your code now to use as_json - it will be one less thing to worry about when you migrate to Rails 3.

This post was inspired by the investigation I went into while exploring the answer to this question on Stack Overflow.

Hey Brian Morearty - Rails was never Javaficated to begin with. So the answer is yes.