Marc Hughes

I am a developer from a bit west of Boston.

Creating nested objects with JSON in Rails

12 May 2009

Did you know it's possible to send a single JSON based request to a stanard rails controller and have it create an object, plus an entire tree of children records?  I knew it was possible, but I was wracking my brain trying to figure out how to do it for several days. Imagine you have a data model something like this:

class Commute < ActiveRecord::Base
  hasmany :locations

class Location < ActiveRecord::Base belongsto :commute end

That's a pretty simple many-to-one relationship.  Now, if you have a regular old Rails controller, how do you pass that single JSON request to create both the commute object, as well as several location children?

The first step is to add the acceptsnestedattributesfor attribute to the commute model object.

class Commute < ActiveRecord::Base
  hasmany :locations
  acceptsnestedattributesfor :locations, :allowdestroy => true
And... that's it on the server side!

To use this functionality, your client should send a PUT request with the following JSON in the body:

    "commute": {
        "minutes": 0,
        "startTime": "Wed May 06 22:14:12 EDT 2009",
        "locationsattributes": [
                "latitude": "40.4220061",
                "longitude": "127.4220061"
                "latitude": "42.4220061",
                "longitude": "41.4220061"
minutes and starttime are two properties of the commute object. And likewise latitude and longitude are two properties of the location objects. The only thing strange there is instead of passing a locations array, you have to pass a locationsattributes array. The "_attributes" is the magic piece that I couldn't figure out.  This lets Rails know you want it to figure out how to create the children, and you're not simply passing those children in.

If you don't put the _attributes suffix in, you'll get errors like the following because Rails is creating a generic hash and trying to shove that into where a Location object should go.

ActiveRecord::AssociationTypeMismatch (Location(#18269790) expected, got HashWithIndifferentAccess(#2654720)):

This general approach should work with XML or HTTP params based requests as well.