32

How do I pass JSON to a RAILS application so it will created nested child objects in a has_many relationship?

Here's what I have so far:

Two model objects.

class Commute < ActiveRecord::Base
  has_many :locations
  accepts_nested_attributes_for :locations, :allow_destroy => true
end

class Location < ActiveRecord::Base
  belongs_to :commute
end

With Commute, I have a standard controller set up. I'd like to be able to create a Commute object as well as several child Location objects in a single REST call using JSON. I've been trying things like this:

curl -H "Content-Type:application/json" -H "Accept:application/json" 
-d "{\"commute\":{\"minutes\":0, 
\"startTime\":\"Wed May 06 22:14:12 EDT 2009\", 
\"locations\":[{\"latitude\":\"40.4220061\",
\"longitude\":\"40.4220061\"}]}}"  http://localhost:3000/commutes

Or more readable, the JSON is:

{
    "commute": {
        "minutes": 0,
        "startTime": "Wed May 06 22:14:12 EDT 2009",
        "locations": [
            {
                "latitude": "40.4220061",
                "longitude": "40.4220061"
            }
        ]
    }
}

When I execute that, I get this output:

Processing CommutesController#create (for 127.0.0.1 at 2009-05-10 09:48:04) [POST]
  Parameters: {"commute"=>{"minutes"=>0, "locations"=>[{"latitude"=>"40.4220061", "longitude"=>"40.4220061"}], "startTime"=>"Wed May 06 22:14:12 EDT 2009"}}

ActiveRecord::AssociationTypeMismatch (Location(#19300550) expected, got HashWithIndifferentAccess(#2654720)):
  app/controllers/commutes_controller.rb:46:in `new'
  app/controllers/commutes_controller.rb:46:in `create'

It looks like the locations JSON array is being read in, but not interpreted as a Location object.

I can easily change either the client or the server, so the solution could come from either side.

So, does RAILS make this easy for me to do? Or do I need to add in some support for this to my Commute object? Perhaps add a from_json method?

Thanks for any help.


As I've been working through this, one solution that works is to modify my controller. But this doesn't seem the "rails" way of doing it, so please still let me know if there's a better way.

def create
    locations = params[:commute].delete("locations");
    @commute = Commute.new(params[:commute])

    result = @commute.save

    if locations 
      locations.each do |location|
        @commute.locations.create(location)
      end
    end


    respond_to do |format|
      if result
        flash[:notice] = 'Commute was successfully created.'
        format.html { redirect_to(@commute) }
        format.xml  { render :xml => @commute, :status => :created, :location => @commute }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @commute.errors, :status => :unprocessable_entity }
      end
    end
  end

2 Answers 2

39

Figured it out, the locations object should have been called locations_attributes to match up with the Rails nested object creation naming scheme. After doing that, it works perfectly with a default Rails controller.

{
    "commute": {
        "minutes": 0,
        "startTime": "Wed May 06 22:14:12 EDT 2009",
        "locations_attributes": [
            {
                "latitude": "40.4220061",
                "longitude": "40.4220061"
            }
        ]
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

You seem to be able to control the source JSON, but what if you could not, how would you handle "locations" ?
Take a look, I just posted an edit from an answer that was deleted.
For anyone still having trouble with this, you must manually permit the attributes in your controller. Example: params.require(:commute).permit( :minutes, :startTime, locations_attributes: [ :latitude, :longitude ])
How to generate locations_attributes instead of locations automatically as an API? Ex: commute.as_json(include: :edificacoes) # => {"commute": {"locations_attributes": [{"latitude": "40.4220061", "longitude": "40.4220061"}]}
0

If you can't easily change the json you're working with, you can just implement new on your models. I'm using the following to wrangle the json into the expected shape, which in my case involves deleting the 'id' key and a few that don't exist in my local model. I have this implemented in application_record.rb and special cases within each model.

 def self.new(properties = {})
  super properties.select { |attr, _val| 
    (['id'] + attribute_types.keys).include?(attr.to_s) 
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.