How to build a JSON response made up of multiple models in Rails

13,570

Solution 1

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.

Solution 2

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.

Share:
13,570
maček
Author by

maček

Updated on June 05, 2022

Comments

  • maček
    maček almost 2 years

    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
    
  • owl
    owl about 14 years
    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"?
  • maček
    maček about 14 years
    @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!\"}"} :(
  • Jonathan Julian
    Jonathan Julian about 14 years
    @ryan fixed hash syntax to be ruby 1.8 style
  • maček
    maček about 14 years
    @Ryan Bigg, it's actually a typo. (And a syntax error, ever for Ruby 1.9). He means {:user => current_user...}
  • Jonathan Julian
    Jonathan Julian about 14 years
    @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.
  • maček
    maček about 14 years
    @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
    maček about 14 years
    See why I had my "Override to_json in Rails 2.3.5" (stackoverflow.com/questions/2572284/…) question before? ;)
  • Jonathan Julian
    Jonathan Julian about 14 years
    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.
  • maček
    maček about 14 years
    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
    maček about 14 years
    @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
    maček about 14 years
    @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 :)