This project is archived and is in readonly mode.

#5374 ✓committed
Jakub Suder

as_json should return a hash for ActiveModel/ActiveRecord objects

Reported by Jakub Suder | August 13th, 2010 @ 01:31 PM | in 3.0.2

Maybe I misunderstood what the as_json method should do, but according to this post http://jonathanjulian.com/2010/04/rails-to_json-or-as_json/, "as_json is used to create the structure of the JSON as a Hash". Currently, as_json simply returns the object as is (def as_json(options = nil); self; end), which kind of defeats the whole purpose of this method.

Here's the Rails 2.3.5 behavior of as_json:

>> User.first.as_json
=> {"created_at"=>Fri Jun 27 05:04:47 -0400 2008, "remember_token_expires_at"=>Sat Feb 20 15:23:11 -0500 2010,
"last_login"=>Thu Jan 21 13:04:06 -0500 2010, "updated_at"=>Thu Jan 21 13:04:16 -0500 2010, "role"=>"admin",
"send_emails"=>false, "id"=>1, "forward_messages"=>true, "password_code"=>nil, "login"=>"admin"}
>> User.first.as_json(:only => :login)
=> {"login"=>"admin"}

And here's the Rails 3 behavior:

> User.last.as_json
=> #<User id: 124, name: "warekus", admin: false, github_username: "", twitter_username: "", ...>
> User.last.as_json(:only => :name)
=> #<User id: 124, name: "warekus", admin: false, github_username: "", twitter_username: "", ...>

Comments and changes to this ticket

  • Jakub Suder

    Jakub Suder August 13th, 2010 @ 02:28 PM

    I think this should fix it (lib/active_model/serializers/json.rb):

    @@ -91,5 +91,5 @@
    
           def as_json(options = nil)
    -        self
    +        serializable_hash(options)
           end
    

    Also, as_json may be overwritten in subclasses and that overwritten version should always be used when generating JSON, so I think this should also be included:

    @@ -81,5 +81,5 @@
           #                    "title": "So I was thinking"}]}
           def encode_json(encoder)
    -        hash = serializable_hash(encoder.options)
    +        hash = as_json(encoder.options)
             if include_root_in_json
               custom_root = encoder.options && encoder.options[:root]
    
  • Rasmus Rønn Nielsen

    Rasmus Rønn Nielsen August 17th, 2010 @ 08:56 PM

    I'm having this problem as well. Jakub's fix works great for me. Thanks Jakub!

  • Jakub Suder

    Jakub Suder August 29th, 2010 @ 08:05 PM

    I'm updating the ticket with a new, extended version of the patch. This got much more complicated than I thought at first, every time I fixed something I kept finding another issue... I probably also fixed this ticket in the process https://rails.lighthouseapp.com/projects/8994/tickets/3087-activesu... (about as_json called on arrays).

    Here's an explanation of what happens in the commit:

    • active_model/serialization.rb - the serializable_hash method was modified so that it doesn't overwrite the values in the options hash that you pass in the argument; in such situation the caller normally doesn't expect the called method to modify the options hash, which could be stored as the caller's instance variable (as is the case with ActiveSupport JSON encoder). Also, I've updated comments because they were misleading (as_json doesn't return a string, to_json does; as_json returns (or should return) a hash).

    • active_model/serializers/json.rb - as_json was changed to return a hash instead of self; the encode_json method was removed because it will never be called (as_json should only return a string, number, array or hash)

    • active_support/json/encoding.rb - lots of changes here:

      • Array#as_json calls as_json on all its elements; but I've noticed that just calling as_json directly breaks a test that checks a case with a circular reference… so I've added added a method jsonify in the encoder, which calls as_json while preventing circular references in the same way this is done in encode
      • Array#encode_json: there's no point in calling as_json on elements again, because the encoder must have called it before encode_json, so we just call encode_json on the elements directly
      • Hash#as_json: I've modified it to also call as_json with options on all its values; I think this is more consistent, because if as_json is supposed to work just like to_json, except it returns a hash instead of string, then it's confusing if to_json passes the options down to its elements and as_json doesn't. I also had to handle the circular reference case here.
      • Hash#encode_json: there were two problems here: one is that we can't make it call as_json on all the values with the original options, because this breaks if ActiveModel returns a representation with a root, like {'user' => { …fields… }}. If we call as_json(:only => :name) on it again, it will return {'user' => {}}, because :name is now called 'name'. On the other hand, I think it is allowed to make a custom as_json method in a model that returns fields like symbols or dates in their original form (the problem is that the contract for as_json isn't defined anywhere, so it's hard to say what exactly it can or should return). So the way I did this is I've added a parameter use_options to encode in encoder, and Hash#encode_json calls encode with use_options = false. That way, the elements are processed with as_json, but without the :only/:except options which could remove some fields.
    • active_record/relation.rb - since I assume in Array#encode_json that as_json has already been called on the elements, I modified as_json in Relation to call as_json on the result from to_a.

    As you can see, there's a lot of stuff happening here, so I'd really appreciate if someone looked through this very carefully. There's also a possibility that I've missed some edge case, because a few times I only found an issue because a test failed, so if something wasn't tested well enough, I might have missed it altogether.

    I'm aware that some of this code might look a bit hacky, but I couldn't think of any better way of writing this and making the tests still pass. Feel free to improve this if you have a better idea…

  • Neeraj Singh

    Neeraj Singh August 30th, 2010 @ 01:41 AM

    • Importance changed from “” to “Low”

    @Jakub. That's a lot of change. It will take me a while to digest all of that.

    It seems the basic assumption of your work is that as_json should return a hash. I disagree.

    I believe as_json should return a ruby object that could be converted into json. The reason why it returns self is because it lets subclasses modify the default representation if the user wants. For example I could write like this

    class User < AR
     def as_json
       [self.id, self.name, self.created_at]
     end
    end
    

    That is a valid representation of as_json. As per your thoughts I must return a hash in as_json method.

    The method that should return a hash is an internal method called serializable_hash.

    I spent some time looking at Ticket #3087 . The fix of that ticket requires change in terms of where in lifecycle serializable_hash is called. Now that rails 3 is out I will work on that ticket again.

  • Jeremy Kemper

    Jeremy Kemper September 6th, 2010 @ 08:17 PM

    • Milestone cleared.
    • State changed from “new” to “open”
  • Repository

    Repository September 7th, 2010 @ 07:36 PM

    • State changed from “open” to “committed”

    (from [2524cf404ce943eca8a5f2d173188fd0cf2ac8b9]) fixed some issues with JSON encoding

    • as_json in ActiveModel should return a hash and handle :only/:except/:methods options
    • Array and Hash should call as_json on their elements
    • json methods should not modify options argument

    [#5374 state:committed]

    Signed-off-by: Jeremy Kemper jeremy@bitsweat.net
    http://github.com/rails/rails/commit/2524cf404ce943eca8a5f2d173188f...

  • Repository

    Repository September 7th, 2010 @ 07:36 PM

    (from [33b954005cd71f1bfba1beca296804ce6c66b0a8]) fixed some issues with JSON encoding

    • as_json in ActiveModel should return a hash and handle :only/:except/:methods options
    • Array and Hash should call as_json on their elements
    • json methods should not modify options argument

    [#5374 state:committed]

    Signed-off-by: Jeremy Kemper jeremy@bitsweat.net

    Conflicts:

    activemodel/lib/active_model/serialization.rb
    

    http://github.com/rails/rails/commit/33b954005cd71f1bfba1beca296804...

  • Jakub Suder

    Jakub Suder September 7th, 2010 @ 09:07 PM

    Thanks!

    Don't forget about the other ticket (https://rails.lighthouseapp.com/projects/8994/tickets/3087) - I think it can be closed too...

    It might be worth mentioning the as_json method in Rails Guides, what do you think? We could add something like this:

    If you need to return an object converted to JSON as a part of a more complex structure, you can use the as_json method which returns a hash representation of the object:

    render :json => {
      :status => 'ok',
      :product => @product.as_json(:except => :id)
    }
    

    to the "2.2.9 Rendering JSON" part of "Layouts and Rendering" guide (http://guides.rubyonrails.org/layouts_and_rendering.html) - it's the only guide I found that has any mention of to_json, so it's probably the best place right now (until someone adds a JSON section in a ActiveModel or ActiveRecord guide).

  • Jeremy Kemper

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

    • Milestone set to 3.0.2

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>

Attachments