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.

  • 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

  • http://jonathanjulian.com/ jjulian

    If you are overriding as_json, make sure you are accepting an options hash and passing it along to super.

  • http://twitter.com/inem Ivan Nemytchenko

    Have you heard about Tequila?
    http://inem.github.com/tequila

  • http://gregword.com Greg

    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).

  • http://jonathanjulian.com/ jjulian

    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/t

  • 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?

  • http://jonathanjulian.com/ jjulian

    Try @people.first.as_json. The options passed to as_json only take :only and :except.

  • http://blog.lawrencepit.com Lawrence Pit

    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.

  • http://twitter.com/tonyamoyal Tony Amoyal

    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?

  • Pingback: Ternary Labs Blog - Migrating to Rails 3.0 Gotchas: as_json bug

  • http://ternarylabs.com Georges Auberger

    Good description. Watch out for a bug in Rails 3.0 tho. Calling super from as_json won't work. Read more http://blog.ternarylabs.com/2010/09/07/migrating-to-rails-3-0-gotchas-as_json-bug/

  • Pingback: Trevor Turk — Links for 9-19-10

  • Stewart

    Can you confirm this works with rails3?

  • http://jonathanjulian.com/ jjulian

    As mentioned above, “Watch out for a bug in Rails 3.0″. I haven't found it reported yet.

  • Pingback: Serving JSON with Rails 3 | |quantumlimit>

  • http://twitter.com/prateekdayal Prateek Dayal

    Hi Jonathan,

    Great blog post. I am not able to use as_json with :include very well. I have posted a questions at http://stackoverflow.com/questions/5000870/defaults-for-to-json-in-rails-with-include (please ignore the comment there about acts_as_api).

    Would appreciate it if you can take a look at it.

    Thanks, Prateek

  • http://spokt.com Dan Hixon
  • Rene

    I experience a huge performance hit, if I override as_json in the model rather than using it directly. Can anybody confirm this? If so, what coul be the reason? Thanks Rene

  • http://padrinorb.com/ Nathan Esquenazi

     Check out http://engineering.gomiso.com/2011/05/16/if-youre-using-to_json-youre-doing-it-wrong/ We agree. to_json / as_json is rarely the right way to go about this.

  • http://padrinorb.com/ Nathan Esquenazi

    Also check out rabl as well: https://github.com/nesquena/rabl

  • Pingback: JSON Schema Baby : Railslove

  • Bob

    Thanks. saved me.

  • Pingback: Rails to_json or as_json? : Mike's burogu

  • Anonymous

    very helpful post, thanks 

  • pseudo_rails

    Given the following example from your blog post:

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

    when overriding the as_json method, if I want to include nested child nodes, how would I go about doing so? I essentially want to refactor the respond_with usage in my controller by just overriding the as_json method in the model.

    class MyController < ApplicationController respond_to :json def show @user = User.find(params[:id]) respond_with(@tag, :include => {:addresses => {:include => :email}}) end

  • pseudo_rails

    Oops, I had a typo in my MyController pseudo code.  

    It should be this:

    class MyController < ApplicationControllerrespond_to :jsondef show@user = User.find(params[:id]) respond_with(@user , :include => {:addresses => {:include => :email}}) end

    Which I want to refactor with something like this in the model: def as_json(options={})super(:only => [:email, :avatar], :include =>[:addresses => {:include => :email}])end

    But I get an  undefined method `macro’ for nil:NilClass

  • Magnemg

    So helpful! thanks. Didn’t know about as_json before.

  • Rafael Revi

    Great post jjulian!

    any chance you can help with http://stackoverflow.com/questions/10433194/rails-format-json