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.

  • What as I don't like about this is that as_json to me is like a view, and should not belong in my model. The options hash only leads to more unnecessary if-then-else branches.
  • How would you make this better? If it's truly going to be a view than you will return everything from as_json and create the object you need in the client side code. So then you can just make as_json skip any attributes you will never need. Do you have a better way to do this?
  • Steve
    This doesn't seem to work when trying to apply it to a collection of models. Such as...

    @people.as_json :only => [:first]

    It just ignores the given options, and renders all of the model's attributes. Any idea how to make that work?
  • Try @people.first.as_json. The options passed to as_json only take :only and :except.
  • Have you heard about Tequila?
    http://inem.github.com/tequila
  • Arpita
    Hi julian , thanks for the article. When i am using as_json with options, it is not honoring the options and returning the entire object.So a call like
    render :json => {:user.as_json(:only => [:email] , :methods => [:getName]} does not work as expected and returns the whole user object and disregards the :methods and :only. Can you suggest any work solution for this?
    Thanks
  • If you are overriding as_json, make sure you are accepting an options hash and passing it along to super.
  • I'm experiencing the same thing that Arpita is. I have a non-ActiveRecord model that I'm trying to serialize with as_json. It looks like this:

    def as_json(options = {})
    { :results => self.results(:only => "Title") }
    end

    where self.results is an array of hashes in the form of:
    [{"Title" => "title1", "Url" => "url1"}, {"Title" => "title2", "Url" => "url2"}]

    I keep getting back both the Title and Url in the json that is generated by simply calling:

    render :json => @object

    from my controller.

    If I change the call on the results array from as_json to to_json, I get back just the "Title" attributes, but there's the encoding problem that you mentioned (extra '\''s in the output).
  • It doesn't look like Array is properly overridden in ActiveSupport 2.3.5 to pass the 'options' to each element. Try this:

    [{"Title" => "title1", "Url" => "url1"}, {"Title" => "title2", "Url" => "url2"}].as_json(:only => "Title")

    (which is broken), vs

    {"Title" => "title2", "Url" => "url2"}.as_json(:only => "Title")

    (which works fine). Please add a comment to ticket 3087!
    https://rails.lighthouseapp.com/projects/8994/tickets/3087-activesupportjsonencode-is-inconsistent-for-as_json-and-to_json
blog comments powered by Disqus