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.
Related
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
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.
I am using the following code snippet from the Rails docs to convert IPs into integers before inserting them into the database:
composed_of :user_ip,
:class_name => 'IPAddr',
:mapping => %w(user_ip to_i),
:constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
:converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
The composed_of block is then followed by this simple validation:
validates_uniqueness_of :user_ip
But the validation in turn throws an error when trying to create a new object:
TypeError: Cannot visit IPAddr
Remove the validation and the error is gone.
If I understand correctly, this is happening because :user_ip becomes an IPAddr object, and that does not sit well with ActiveRecord. Is this correct, and if so, is there a way around it?
Not sure if you ever found a solution to this, but I was able to patch it in my gem by adding a visit method to Arel.
You should be able to do something like the following to get it to work. This is based on how Arel converts values for other types of objects such as dates. Not sure if you need to convert the value to a string, but it might just work as an integer.
module Arel
module Visitors
class ToSql
def visit_IPAddr
quote(value.to_i)
end
end
end
end
I needed to be able to dynamically generate these methods so I used the following in my gem:
Arel::Visitors::ToSql.class_eval do
define_method "visit_#{klass.name}", lambda {|value| quote(value.to_s) }
end
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/
I have a ROXML object that looks like:
class Activity
include ROXML
xml_accessor :id
end
If I have an array of these objects and call .to_xml on the array, I receive an empty xml collection:
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<activities type=\"array\">\n</activities>\n"
Any idea why or how to fix this?
I'm running Rails 2.3.5 with the newest version of ROXML.
Similar question: Ruby ROXML - how to get an array to render its xml?. Look there for details.
It seems that it is not possible to convert an array using ROXML. One way to solve this is to define new class for collection:
class Activities
include ROXML
def initialize(activities)
#activities = activities
end
xml_reader :activities, :as => [Activity]
end
And use:
Activities.new(activities).to_xml