12

First, the desired result

I have User and Item models. I'd like to build a JSON response that looks like this:

{
  "user":
    {"username":"Bob!","foo":"whatever","bar":"hello!"},

  "items": [
    {"id":1, "name":"one", "zim":"planet", "gir":"earth"},
    {"id":2, "name":"two", "zim":"planet", "gir":"mars"}
  ]
}

However, my User and Item model have more attributes than just those. I found a way to get this to work, but beware, it's not pretty... Please help...

Update

The next section contains the original question. The last section shows the new solution.


My hacks

home_controller.rb

class HomeController < ApplicationController

  def observe
    respond_to do |format|
      format.js { render :json => Observation.new(current_user, @items).to_json }
    end
  end

end

observation.rb

# NOTE: this is not a subclass of ActiveRecord::Base
# this class just serves as a container to aggregate all "observable" objects
class Observation
  attr_accessor :user, :items

  def initialize(user, items)
    self.user = user
    self.items = items
  end

  # The JSON needs to be decoded before it's sent to the `to_json` method in the home_controller otherwise the JSON will be escaped...
  # What a mess!
  def to_json
    {
      :user => ActiveSupport::JSON.decode(user.to_json(:only => :username, :methods => [:foo, :bar])),
      :items => ActiveSupport::JSON.decode(auctions.to_json(:only => [:id, :name], :methods => [:zim, :gir]))
    }
  end
end

Look Ma! No more hacks!

Override as_json instead

The ActiveRecord::Serialization#as_json docs are pretty sparse. Here's the brief:

as_json(options = nil) 
  [show source]

For more information on to_json vs as_json, see the accepted answer for Overriding to_json in Rails 2.3.5

The code sans hacks

user.rb

class User < ActiveRecord::Base

  def as_json(options)
    options = { :only => [:username], :methods => [:foo, :bar] }.merge(options)
    super(options)
  end

end

item.rb

class Item < ActiveRecord::Base

  def as_json(options)
    options = { :only => [:id, name], :methods => [:zim, :gir] }.merge(options)
    super(options)
  end

end

home_controller.rb

class HomeController < ApplicationController

  def observe
    @items = Items.find(...)
    respond_to do |format|
      format.js do
        render :json => {
          :user => current_user || {},
          :items => @items
        }
      end
    end
  end

end
Community
  • 1
  • 1
maček
  • 76,434
  • 37
  • 167
  • 198

4 Answers4

10

EDITED to use as_json instead of to_json. See How to override to_json in Rails? for a detailed explanation. I think this is the best answer.

You can render the JSON you want in the controller without the need for the helper model.

def observe
  respond_to do |format|
    format.js do
      render :json => {
        :user => current_user.as_json(:only => [:username], :methods => [:foo, :bar]),
        :items => @items.collect{ |i| i.as_json(:only => [:id, :name], :methods => [:zim, :gir]) }
      }
    end
  end
end

Make sure ActiveRecord::Base.include_root_in_json is set to false or else you'll get a 'user' attribute inside of 'user'. Unfortunately, it looks like Arrays do not pass options down to each element, so the collect is necessary.

Community
  • 1
  • 1
Jonathan Julian
  • 12,163
  • 2
  • 42
  • 48
  • The hash syntax you're using is only from 1.9 and may confuse anyone who's not familiar with it. May I suggest changing it to be the standard `"user" => current` that we all "know and love"? – Ryan Bigg Apr 03 '10 at 19:23
  • @Jonathan Julian, `ActiveRecord::Base.include_root_in_json` is set to `false` and this is doing exactly what I *expected*, but not exactly what I *hoped* for. The internal `to_json` calls are getting escaped by `render :json`. For example, instead of `{"user": {"username": "Bob!"}}` I am getting `{"user": "{\"username\": \"Bob!\"}"}` :( – maček Apr 03 '10 at 19:29
  • @ryan fixed hash syntax to be ruby 1.8 style – Jonathan Julian Apr 03 '10 at 19:30
  • @Ryan Bigg, it's actually a typo. (And a syntax error, ever for Ruby 1.9). He means `{:user => current_user...}` – maček Apr 03 '10 at 19:32
  • @smotchkkisss You can always `render :json => {}` and just build up that hash by hand *without* calling to_json on the models. Or use `decode` as you've already found. Either way, there's no need for a separate model. – Jonathan Julian Apr 03 '10 at 19:38
  • @Jonathan Julian, thanks for giving this a shot. You might want to make a "does not work" note somewhere in your answer so people don't chase down the same dead end I did. +1 for effort – maček Apr 03 '10 at 19:46
  • @Ryan Bigg, I looked into this new syntax a bit. `{a: "foo"}` is valid, `{'a': "foo"}` is not. Like you, I still prefer the `{:foo => "bar"}` notation :) – maček Apr 03 '10 at 21:36
  • @Jonathan Julian, thanks again. I updated the original question to show the application of the new `as_json` override as well. This make me very happy :) – maček Apr 04 '10 at 18:29
1

Incase anyone is looking for an alternative solution for this, this is how I solved this in Rails 4.2:

def observe
  @item = some_item
  @user = some_user

  respond_to do |format|
    format.js do
      serialized_item = ItemSerializer.new(@item).attributes
      serialized_user = UserSerializer.new(@user).attributes
      render :json => {
        :item => serialized_item, 
        :user => serialized_user
      }
    end
  end
end

This returns the serialized version of both objects as JSON, accessible via response.user and response.item.

0

There are a lot of new Gems for building JSON now, for this case the most suitable I have found is Jsonify:

https://github.com/bsiggelkow/jsonify https://github.com/bsiggelkow/jsonify-rails

This allows you to build up the mix of attributes and arrays from your models.

mtkd
  • 404
  • 4
  • 4
-1

Working answer #2 To avoid the issue of your json being "escaped", build up the data structure by hand, then call to_json on it once. It can get a little wordy, but you can do it all in the controller, or abstract it out to the individual models as to_hash or something.

def observe
  respond_to do |format|
    format.js do
      render :json => {
        :user => {:username => current_user.username, :foo => current_user.foo, :bar => current_user.bar},
        :items => @items.collect{ |i| {:id => i.id, :name => i.name, :zim => i.zim, :gir => i.gir} }
      }
    end
  end
end
Jonathan Julian
  • 12,163
  • 2
  • 42
  • 48
  • See why I had my "Override `to_json` in Rails 2.3.5" (http://stackoverflow.com/questions/2572284/override-to-json-in-rails-2-3-5) question before? ;) – maček Apr 03 '10 at 20:10
  • To solve both your problems, create a `to_hash` in each of your models and build them yourself. Trade off a bit of code for a couple headaches. – Jonathan Julian Apr 03 '10 at 20:18
  • please update `@items.each` to read `@items.collect`. `Array#each` returns the original `@items` array. This is only mildly cleaner than what I had before, but it still seems like I should be able to tap into the `as_json` or `to_json` methods somehow; I mean, that's what they're for. It's very possible that many more objects will be appearing in the "Observation" hash, so doing all this extra work for each one seems like it could be a headache of its own. If a more suitable answer doesn't appear in a couple days, I'll mark this as accepted. Thanks again :) – maček Apr 03 '10 at 20:35