Rails, including relationships in json render - ruby-on-rails

I have the following code which returns a json with some data that includes #admin_user.companies:
#admin_user = User.find_by_email(email)
render :json=>{:status=>{:code=>200,:token=>#admin_user.authentication_token,
:user=> #admin_user,
:companies => #admin_user.companies }}
Each company also have many "locations". How do I include all the locations for every single company in #admin_user.companies in the json?

The conventional way is to use
render json: #admin_user.companies, include: :locations
(Please refer to #as_json for more options.)
You don't need to include the status code in your JSON, since it's already in the HTTP headers. Thus, the following might get you close to what you need.
render :json => #admin_user,
:include => {
:companies => { :include => :locations },
},
:methods => :authentication_token
Side Note
This is just an example. You will have to configure :include and :methods to get exactly what you want. For even more fine-grained control, look into JBuilder or RABL.

Another approach is to use jbuilder which allows you much finer-grained control over how your JSON is generated:
Add the jbuilder gem to your app
Use JBuilder.encode from your controllers to generate JSON
Alternatively, create .json.jbuilder templates in your views directories

Related

Downgrading rails project to Ruby 1.9.3 | amusing .to_json trouble

Dear stackoverflow people,
I want to downgrade a rails project so it can run on rails 3 without any problems. It worked before on the newest version of rails, but the office does not want to use this one. I have trouble with rewriting this particular line:
#results = #sessions.to_json(:include => [:orientations, :subtopics, :data_files, :participants, :formats, :comments => {:include => [:user => {:only => [:id, :name]}]}])
#sessions are of course a list of results. Is there anyone who knows how I can write an alternative for this that will also run on older versions of rails?
Thanks in advance
Kind regards
Here you go:
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 decoratively.
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 or Ruby 1.9.3.

Make ActiveResource models generate hashes for nested objects

In my application, I‘m using ActiveResource to manage the data that I receive from a remote API. Say, one of my models is called Account and it has a field called settings, which is documented in the API as a “freeform hash”, meaning it can store whatever.
A sample simplified JSON I would receive from the API:
{
"account": {
"title": "My Account",
"settings": {
"a_random_setting": true,
"another_random_setting": 42,
"a_subconfig_of_sorts": {
"is_deep": true
}
}
}
}
ActiveResource very kindly goes all the way down the deepest nested objects in that JSON and turns them all into Ruby objects:
my_account.settings # => Account::Settings
my_account.settings.a_subconfig_of_sorts # => Account::Settings::ASubconfigOfSorts
This makes it a bit difficult to look up dynamic keys within that the content of settings. Essentially, I would much rather have settings as regular hash, and not a custom nested object generated for me on the fly.
my_account.settings.class # => Hash
my_account.settings[:a_subconfig_of_sorts] # => {:is_deep => true}
How do I make ActiveResource do that? My first guess was by using the schema declaration, but that only provides scalar types, it seems.
Made solution that works with that issue. Hope that helps.
class Account < ActiveResource::Base
create_reflection :settings_macro, :settings, class_name: 'Account::Hash'
class Hash < ::Hash
def initialize(hash, persisted)
merge!(hash)
end
end
end

Rails as_json issue - How to include nested objects efficiently?

I'm running into an issue where I'm working with the as_json method, and how to efficiently return the object in JSON AND it's belongs_to object as JSON as well, where the belongs_to object has its own belongs_to object. Code would probably explain it better.
The Not-Working Way
Alert class
class Alert < ActiveRecord::Base
belongs_to :message
# for json rendering
def as_json(options={})
super(:include => :message)
end
end
Message class
def as_json(options={})
super( methods: [:timestamp, :num_photos, :first_photo_url, :tag_names],
include: { camera: { only: [:id, :name] },
position: { only: [:id, :name, :address, :default_threat_level ]},
images: { only: [:id, :photo_url, :is_hidden]} })
end
The problem with this first set up is that when I have an Alert object and call
alert.as_json()
I get all the attributes from Alert and all the attributes from Message, but none of the other attributes from Message that I want, like Camera, Position, etc.
Here's the "It's Working, But Probably Not Proper Design Way"
Alert Class
class Alert < ActiveRecord::Base
belongs_to :message
# for json rendering
def as_json(options={})
super().merge(:message => message.as_json)
end
end
Messages Class
# for json rendering
def as_json(options={})
super( methods: [:timestamp, :num_photos, :first_photo_url, :tag_names])
.merge(:camera => camera.as_json)
.merge(:position => position.as_json)
.merge(:images => images.as_json)
end
In this 2nd setup, I get all of Messages's nested attributes like I want.
My question, am I missing some Rails Convention to do this properly? It seems like there would/should be an easier way.
The best answer for me was using serializable_hash. #kikito touched on this in his comment, but there was a typo that prevented it from working. It's not serialized_hash, it's serializable_hash.
Literally just find + replace as_json with serializable_hash and this bug goes away. (It's still not fixed in today's Rails 4.0.2). You also get the benefit of having an easier time implementing an XML API later (some people still use those!).
Which version of Rails are you using? This is a known bug in older versions of Rails, supposedly fixed with this pull request. Your syntax looks right to me, so perhaps this is your problem?
As an aside, you may also want to checkout the new active_model_serializers from Jose Valim (Rails core member). It may at least enable you to work around your issue in a more elegant manner.
I would recommend you to take a look at RABL (stands for Ruby API Builder Language) gem (railscast, github). It offers you a DSL for defining the structure of your JSON (and also XML) response in templates (like Haml or CoffeeScript does). It also supports partials.

to_xml with :only and :methods

I'm calling to_xml on an ActiveRecord object with both :only and :methods parameters.
The method that I'm including returns a collection for AR objects. This works fine without the :only param, but when that is added I just get the default to_s representation of my objects.
i.e
<author><books>#<Book:0x107753228></books>\n</author>
Any ideas?
Update, here is the code:
class Author < ActiveRecord::Base
def books
#this is a named scope
products.by_type(:book)
end
end
Author.to_xml(:methods => :books, :only => :id)
AFAIK, you have to handle the child objects by hand:
a = Author.find_by_whatever
xml_string = a.to_xml(:only => :id) { |xml|
a.books.to_xml(:builder => xml, :skip_instruct => true)
}
The :skip_instruct flag tells the builder to leave out the usual <?xml version="1.0" encoding="UTF-8"?> XML preamble on the inner blob of XML.
The XML serializer won't call to_xml recursively, it just assumes that everything from :methods is simple scalar data that should be slopped into the XML raw.
Off the top of my head, I think you can do:
Author.to_xml(:methods => {:books => {:only => :id}})
I've used something similar when including AR associations into the XML, not so sure about custom methods though.

as_json not calling as_json on associations

I have a model with data that should never be included when it is rendered as json. So I implemented the class' as_json method to behave appropriately. The problem is when other models with associations with this model render json, my custom as_json is not being called.
class Owner < ActiveRecord::Base
has_one :dog
def as_json(options={})
puts "Owner::as_json"
super(options)
end
end
class Dog < ActiveRecord::Base
belongs_to :owner
def as_json(options={})
puts "Dog::as_json"
options[:except] = :secret
super(options)
end
end
Loading development environment (Rails 3.0.3)
ruby-1.9.2-p136 :001 > d = Dog.first
=> #<Dog id: 1, owner_id: 1, name: "Scooby", secret: "I enjoy crapping everwhere">
ruby-1.9.2-p136 :002 > d.as_json
Dog::as_json
=> {"dog"=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1}}
ruby-1.9.2-p136 :004 > d.owner.as_json(:include => :dog)
Owner::as_json
=> {"owner"=>{"id"=>1, "name"=>"Shaggy", :dog=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1, "secret"=>"I enjoy crapping everwhere"}}}
Thanks for the help
This is a known bug in Rails. (The issue is marked closed due to the migration to Github issues from the previous bug tracker, but it's still a problem as of Rails 3.1.)
As acknowledged above, this is an issue with the Rails base. The rails patch here is not yet applied and seems at least slightly controversial, so I'm hesitant to apply it locally. Even if applied as a monkey patch it could potentially complicate future rails upgrades.
I'm still considering RABL suggested above, it looks useful. For the moment, I'd rather not add another view templating language into my app. My current needs are very small.
So here's a workaround which doesn't require a patch and work for most simple cases. This works where the association's as_json method you'd like to have called looks like
def as_json(options={})
super( <... custom options ...> )
end
In my case I've got Schedule model which has many Events
class Event < ActiveRecord::Base
# define json options as constant, or you could return them from a method
EVENT_JSON_OPTS = { :include => { :locations => { :only => [:id], :methods => [:name] } } }
def as_json(options={})
super(EVENT_JSON_OPTS)
end
end
class Schedule < ActiveRecord::Base
has_many :events
def as_json(options={})
super(:include => { :events => { Event::EVENT_JSON_OPTS } })
end
end
If you followed the guideline that anytime you :include an association in your as_json() methods, you define any options you need as a constant in the model to be referenced, this would work for arbitrary levels of associations. NOTE I only needed the first level of association customized in the above example.
I've found that serializable_hash works just as you'd expect as_json to work, and is always called:
def serializable_hash(options = {})
result = super(options)
result[:url] = "http://.."
result
end
I ran into the same issue. I wanted this to work:
render :json => #favorites.as_json(:include => :location)
But it didn't so I ended up adding an index.json.erb with the following:
<% favs = #favorites.as_json.each do |fav| %>
<% fav["location"] = Location.find(fav["location_id"]).as_json %>
<% end %>
<%= favs.to_json.html_safe %>
Not a fix - just a work around. I imagine you did the same thing.
Update #John pointed out this is a known bug in Rails. A patch to fix it appears to be: at https://github.com/rails/rails/pull/2200. Nevertheless, you might try RABL, because its sweet.
I've always been frustrated with passing a complex set of options to create the JSON views I want. Your problem, which I experienced with Mongoid in Rails 3.0.9, prompted me to write JSON templates. But actually, if you're dealing with relations or custom api properties, it turns out that templates are way nicer.
Besides, dealing with different outputs seems like the View layer to me, so I settled on using RABL, the API templating language. It makes it super easy to build valid JSON and include any associations or fields.
Not a solution to the problem, but a better solution for the use case.
This was reported as a bug: http://ternarylabs.com/2010/09/07/migrating-to-rails-3-0-gotchas-as_json-bug/

Resources