This project is archived and is in readonly mode.

#3087 ✓resolved
James Herdman

ActiveSupport::JSON.encode is Inconsistent for #as_json and #to_json

Reported by James Herdman | August 21st, 2009 @ 08:43 PM | in 3.0.2

Suppose you define your #as_json method as such:

class Something < ActiveRecord::Base
  def as_json(options={})
    { :foo => "bar", :baz => "balloon" }
  end
end

If you have an array of these objects and then encode them to JSON via ActiveSupport::JSON.encode, you get a collection of the object converted to JSON in which #as_json is ignored.

Suppose now you define a #to_json method (old school, yeah) as such:

class Something < ActiveRecord::Base
  def to_json(options={})
    super(:methods => [:foo, :baz])
  end
end

Like before, you encode an Array of objects to JSON via ActiveSupport::JSON.encode. In this scenario you will get a JSON object as you have defined via #to_json.

It's my understanding that we're supposed to be moving to #as_json for Rails 3 compatibility.

Comments and changes to this ticket

  • Jonathan Julian

    Jonathan Julian April 4th, 2010 @ 04:45 PM

    Another way to describe this: as_json options are not passed down to each element of an array of ActiveRecord objects. In the first example, all the attributes of user are rendered, which is not what I expect.

    format.json { render :json => @users.as_json(:only => [:email]) }
    

    I need to do this hack so that only the email attribute is rendered.

    format.json { render :json => @users.collect{ |u| u.as_json(:only => [:email]) } }
    

    I can reproduce this with ActiveRecord and ActiveSupport 2.3.5.

  • Jonathan Julian

    Jonathan Julian May 18th, 2010 @ 02:45 PM

    I think ActiveSupport 2.3.x needs to provide an as_json implementation that calls as_json(options) on each array element. This is not limited to ActiveRecord.

    [{"k" => "1", "x" => "1"}, {"k" => "2", "x" => "2"}].as_json(:only => "k")
     => [{"k"=>"1", "x"=>"1"}, {"k"=>"2", "x"=>"2"}]
    [{"k" => "1", "x" => "1"}, {"k" => "2", "x" => "2"}].collect { |element| element.as_json(:only => "k") }
     => [{"k"=>"1"}, {"k"=>"2"}]
    
  • Rizwan Reza

    Rizwan Reza May 21st, 2010 @ 12:49 AM

    • no changes were found...
  • Ola Tuvesson

    Ola Tuvesson June 22nd, 2010 @ 03:17 AM

    I have the same issue (Rails3b3, Ruby 1.9.2):

    format.json { render :json => @listings.as_json(:only => [:name]) }
    
    => [{id: 1, name: "ACME", type: 3},{id: 2, name: "Apple", type: 7}]
    

    And

    format.json { render :json => @listings.to_json(:only => [:name]) }
    
    => ArgumentError: wrong number of arguments (2 for 1)
    

    And

    format.json { render :json => @listings.collect{|u| 
      u.as_json(:only => [:name]) 
    }}
    
    => [{id: 1, name: "ACME", type: 3},{id: 2, name: "Apple", type: 7}]
    

    But

    format.json { render :json => @listings.collect{|u| 
      u.to_json(:only => [:name]) 
    }}
    
    => ["{"name":"ACME"},{"name":"Apple"}"]
    

    Also, as you can see, looping over each element also inserts extra quotes.

  • Huet Bartels

    Huet Bartels June 30th, 2010 @ 03:30 AM

    • Importance changed from “” to “High”

    I am also having the same issues with 3.0.0.beta3 with ruby 1.9.1p378.

  • Neeraj Singh

    Neeraj Singh June 30th, 2010 @ 04:13 AM

    • Assigned user set to “José Valim”

    @Huet most likely you already know that rails3 does not support ruby 1.9.1. Officially rails3 is supported on ruby 1.9.2 .

    Assigning it to Mr. Valim to get his input on the design strategy behind to_json and as_json and to clarify which one end users should be using.

    It is my understanding that as_json is implementation detail and end users (not inside rails) should be using to_json.

  • José Valim

    José Valim June 30th, 2010 @ 06:14 AM

    • State changed from “new” to “invalid”

    as_json should return an object to be represented in json. You should always overwrite as_json in your module, but always call to_json.

    About the bug, have you tried on Rails master? There was a bug related with the json gem overwriting Rails behavior which was fixed a few days ago. That fixed, for instance, the ArgumentError a few reported above.

    I'm glad to reopen if this is still an issue.

  • Jeremy Kemper

    Jeremy Kemper June 30th, 2010 @ 06:17 AM

    • Milestone set to 2.3.9
    • State changed from “invalid” to “open”

    Implementing as_json is right. Using to_json is a mess because JSON libraries use it for their internal encoding API.

    This is similar to the Marshal library: you implement _dump to return a primitive Ruby object to marshal. You're responsible for returning a marshalable representation of your object, not the action marshaled bytes.

    Similarly for JSON: you implement as_json to return a JSON representation of your object. The actual work of building a JSON string is handled by internal methods.

    This is a clearer, cleaner separation of responsibility than using a single to_json method for both providing JSON representations and for building JSON strings.

    So yes, this is a bug :)

  • Jeremy Kemper

    Jeremy Kemper June 30th, 2010 @ 06:19 AM

    • State changed from “open” to “duplicate”

    Reviewing the examples above, this was fixed. Please verify on latest master!

  • José Valim

    José Valim June 30th, 2010 @ 06:23 AM

    • State changed from “duplicate” to “open”

    As JK assigned, if someone wants to backport the fix from master to 2-3-stable, a patch is welcome. The original commit is:

    http://github.com/rails/rails/commit/7bd85a8fc2d216a5e2b1d0380df572...

  • Neeraj Singh

    Neeraj Singh June 30th, 2010 @ 05:10 PM

    • Tag set to to_json

    Mr. Valim,

    Looks like the patch you provided is not working for me in rails3. Here is the breakdown.

    ActiveRecord::Schema.define(:version => 20100630160235) do
    
      create_table "users", :force => true do |t|
        t.string   "name"
        t.datetime "created_at"
        t.datetime "updated_at"
      end
    
    end
    
    class User < ActiveRecord::Base
    end
    
    class UsersController < ApplicationController
      def index
        User.delete_all
        User.create(:name => 'John')
        User.create(:name => 'Mary')
        @users = User.all
    
        respond_to do |format|
          format.html # index.html.erb
          format.xml  { render :xml => @users }
          format.json  { render :json => @users.as_json(:only => :name) }
        end
      end
    end
    
    format.json  { render :json => @users.as_json(:only => :name) }
    [{"user":{"name":"John","created_at":"2010-06-30T16:07:42Z","updated_at":"2010-06-30T16:07:42Z","id":9}},{"user":{"name":"Mary","created_at":"2010-06-30T16:07:42Z","updated_at":"2010-06-30T16:07:42Z","id":10}}]
    
    #If I change to to_json from as_json then it is working great
    
    format.json  { render :json => @users.to_json(:only => :name) }
    [{"user":{"name":"John"}},{"user":{"name":"Mary"}}]
    
    # once again note that I conducted this test on rails edge and it is failing
    
  • Neeraj Singh

    Neeraj Singh June 30th, 2010 @ 05:40 PM

    my bad. to_json should be used in this case and not as_json. So rails3 is working fine.

    back to rails2.

  • Neeraj Singh

    Neeraj Singh June 30th, 2010 @ 05:43 PM

    I am not able to reproduce the issue in rails 2-3-master branch. Here is what I did

    ActiveRecord::Schema.define(:version => 20100630152421) do
    
      create_table "users", :force => true do |t|
        t.string   "name"
        t.datetime "created_at"
        t.datetime "updated_at"
      end
    
    end
    class User < ActiveRecord::Base
      def as_json(options={})
        { :name => self.name.downcase }
      end
    end
    
    
    class UsersController < ApplicationController
      def index
        User.delete_all
        User.create(:name => 'JOHN')
        User.create(:name => 'MARY')
        @users = User.all
    
        respond_to do |format|
          format.html # index.html.erb
          format.xml  { render :xml => @users }
          format.json  { render :json => @users }
        end
      end
    end
    
    # localhost:3000/user.json results in [{"name":"john"},{"name":"mary"}]
    
  • Jonathan Julian

    Jonathan Julian June 30th, 2010 @ 05:55 PM

    @Neeraj: Try this: remove the as_json definition from your AR class, and change your controller to do:

    format.json  { render :json => @users.as_json(:only => [:name]) }
    

    You'll see that the resulting JSON includes all 3 attributes of each User, instead of just :name, which was expected. Calling as_json from a controller is useful when many different controllers each need a different representation and you don't want to code just one into the model.

  • Neeraj Singh

    Neeraj Singh June 30th, 2010 @ 06:04 PM

    May be I am still not getting as_json vs to_json thing. Based on what JK and JV said, I gather than finally to_json should be invoked on the object. as_json should be used to configure what should be the final output.

    As I have show in my example above , I used as_json in the model to configure that I only want name attributes and I am invoking to_json on the final set of collection.

    JK/JV help me again please :-)

  • Neeraj Singh

    Neeraj Singh June 30th, 2010 @ 06:37 PM

    • State changed from “open” to “invalid”
    • installed latest json gem
    • Successfully installed json-1.4.3
    • if I use @users.to_json(:only => :name) then every thing works fine.

    Whether to use to_json or as_json is little confusing. As JK and JV mentioned above, as_json is means to get to right to_json output. as_json should be used in the model to change the behavior of the model when to_json is invoke.

    by invoking @users.as_json(), to_json will NEVER get invoked and that is wrong.

    I am closing this issue. If there is any question please feel free to post.

    I do believe that there should be a rails guide/blog about when to use as_json and when to use to_json.

  • Jeremy Kemper

    Jeremy Kemper June 30th, 2010 @ 06:44 PM

    Users always call foo.to_json or ActiveSupport::JSON.encode(foo)

    The implementor of Foo writes an as_json

  • Jonathan Julian

    Jonathan Julian June 30th, 2010 @ 06:50 PM

    Ack! Don't close this!

    Remember that the caller can get a hash representation by invoking as_json by hand, passing the options hash. This ticket describes the problem that the options are not being passed to each element of an association! Make my code change as described in comment "June 30th, 2010 @ 05:55 PM" to demonstrate the issue.

    And here is an outsider's explanation of when to use as_json and to_json: http://jonathanjulian.com/2010/04/rails-to_json-or-as_json/

  • Neeraj Singh

    Neeraj Singh June 30th, 2010 @ 07:00 PM

    'Jonathan Julian' got everything right.

    Regarding the options not being passed , checkout my test.

    ActiveRecord::Schema.define(:version => 20100630152421) do
    
      create_table "users", :force => true do |t|
        t.string   "name"
        t.datetime "created_at"
        t.datetime "updated_at"
      end
    
    end
    class User < ActiveRecord::Base
    end
    
    
    class UsersController < ApplicationController
      def index
        User.delete_all
        User.create(:name => 'JOHN')
        User.create(:name => 'MARY')
        @users = User.all
    
        respond_to do |format|
          format.html # index.html.erb
          format.xml  { render :xml => @users }
          format.json  { render :json => @users }
        end
      end
    end
    
    # with format.json  { render :json => @users.to_json(:only => :name) }
    # localhost:3000/user.json results in [{"name":"john"},{"name":"mary"}]
    
    # with format.json  { render :json => @users.to_json(:only => [:name, :created_at) }
    # localhost:3000/user.json results in [{"user":{"name":"JOHN","created_at":"2010-06-30T17:59:11Z"}},{"user":{"name":"MARY","created_at":"2010-06-30T17:59:11Z"}}]
    
  • pedz

    pedz July 13th, 2010 @ 02:06 AM

    • Tag changed from to_json to activerecord associations, to_json

    I am having problems with associations. They do not seem to honor either the to_json or the as_json methods within the child models.

    
    ActiveRecord::Schema.define(:version => 20100713002653) do
    
      create_table "boos", :force => true do |t|
        t.string   "name"
        t.datetime "created_at"
        t.datetime "updated_at"
      end
    
      create_table "foos", :force => true do |t|
        t.string   "name"
        t.integer  "boo_id"
        t.datetime "created_at"
        t.datetime "updated_at"
      end
    
    end
    class Boo < ActiveRecord::Base
      has_many :foos
    end
    class Foo < ActiveRecord::Base
      belongs_to :boo
    
      def as_json(option = {})
        super({ :only => [ :name ] } )
      end
    end
    class BoosController < ApplicationController
      def index
        b1 = Boo.create(:name => "Tom");
        b1.foos.create(:name => "Alice");
        b1.foos.create(:name => "Jane");
        b2 = Boo.create(:name => "George");
        b2.foos.create(:name => "Suzy");
        b2.foos.create(:name => "Jenny");
        render :json => Boo.find(:all).to_json({ :include => [ :foos ] })
      end
    end
    
    curl localhost:3000/boos.json
    [{"boo":{"created_at":"2010-07-13T00:52:35Z","id":1,"name":"Tom","updated_at":"2010-07-13T00:52:35Z","foos":[{"boo_id":1,"created_at":"2010-07-13T00:52:35Z","id":1,"name":"Alice","updated_at":"2010-07-13T00:52:35Z"},{"boo_id":1,"created_at":"2010-07-13T00:52:35Z","id":2,"name":"Jane","updated_at":"2010-07-13T00:52:35Z"}]}},{"boo":{"created_at":"2010-07-13T00:52:35Z","id":2,"name":"George","updated_at":"2010-07-13T00:52:35Z","foos":[{"boo_id":2,"created_at":"2010-07-13T00:52:35Z","id":3,"name":"Suzy","updated_at":"2010-07-13T00:52:35Z"},{"boo_id":2,"created_at":"2010-07-13T00:52:35Z","id":4,"name":"Jenny","updated_at":"2010-07-13T00:52:35Z"}]}}]
    

    I would expect all the foos to have only the names.
    I also don't understand why the as_json only takes :only and :except. How am I suppose to include associations from a controller if to_json is not suppose to be used?

    (I'm using Rails 2.3.5)

  • Neeraj Singh

    Neeraj Singh July 16th, 2010 @ 08:24 PM

    • Milestone changed from 2.3.9 to 3.x
    • State changed from “invalid” to “open”
    • Tag changed from activerecord associations, to_json to activerecord associations, patch, to_json
    • Importance changed from “High” to “Low”

    @pedz Attached is a patch that fixes the issue that you mentioned. Please try it and let us know if it works for you.

  • José Valim

    José Valim July 18th, 2010 @ 10:27 AM

    • State changed from “open” to “invalid”

    Neeraj, the test in your patch was supposed to be calling super(:only => :size), no? I don't think the following is/should be supported:

    def as_json
      { :only => :size }
    end
    

    I don't even think as_json should consider such options as :only and :except. Or you either do it manually:

    def as_json
      super.slice("only")
    end
    

    Or you overwrite to_json to change how to encode it (the representation in as_json is still the same):

    def to_json
      super(:only => :size)
    end
    
  • Neeraj Singh

    Neeraj Singh July 18th, 2010 @ 01:11 PM

    • State changed from “invalid” to “open”

    Attached is a failing test case.

  • José Valim

    José Valim July 19th, 2010 @ 02:32 PM

    This is a valid test case for me! And the fix provided in the previous patch looks good to me as well. Could you please bundle then in one package Neeraj? :)

  • Neeraj Singh

    Neeraj Singh July 19th, 2010 @ 02:44 PM

    Fix provided in previous case is not solving the problem mentioned in the failing test. it is much more complicated.

    AM uses serializable_hash and then builds the JSON output. However there is no easy way , at least I have not found, to invoke as_json from within serializable_hash.

    Also serializable_hash is a higher level abstraction which is used by both to_xml and to_json. I need to spend some time to understand the existing design before I could come up with a solution. This ticket is in my todo list for today. Will see how it goes :-)

  • pedz

    pedz July 21st, 2010 @ 11:43 PM

    I'm not at edge rails but I will look at the patch. I'm not sure when I'll be able to move.

  • Jeremy Kemper
  • Jeremy Kemper

    Jeremy Kemper September 7th, 2010 @ 09:14 PM

    • State changed from “open” to “resolved”
  • Rohit Arondekar

    Rohit Arondekar October 7th, 2010 @ 05:21 AM

    • Tag changed from activerecord associations, patch, to_json to activerecord, associations, patch, to_json
  • Jeremy Kemper

    Jeremy Kemper October 15th, 2010 @ 11:01 PM

    • Milestone set to 3.0.2
  • csnk

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

<h2 style="font-size: 14px">Tickets have moved to Github</h2>

The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>

Referenced by

Pages