24

I've written an API with Rails and need to accept some nested_attributes in API calls.

Currently I send data via

PATCH /api/v1/vintages/1.json

{
  "vintage": {
    "year": 2014,
    "name": "Some name",
    "foo": "bar",
    "sizes_attributes": [
      {
        "name": "large",
        "quantity": "12"
      },
      {
        "name": "medium",
        "quantity": "2"
      }
    ]
  }
}

However, I'd like to perform the following:

PATCH /api/v1/vintages/1.json

{
  "vintage": {
    "year": 2014,
    "name": "Some name",
    "foo": "bar",
    "sizes": [
      {
        "name": "large",
        "quantity": "12"
      },
      {
        "name": "medium",
        "quantity": "2"
      }
    ]
  }
}

The difference being attributes being part of the key of the fields. I want to be able to accept_nested_attributes_for :sizes without having to use _attributes be a part of the JSON object.

Anyone know how to manage this?

1
  • well it would be hackish, but couldn't just do something params[:vintage][:sizes_attributes] = params[:vintage][:sizes] to rename them. Commented Dec 5, 2014 at 19:28

2 Answers 2

32
+50

You are free to perform some magic in your strong parameters methods. Based on what you want, you likely have this method in your controller:

def vintage_params
  params.require(:vintage).permit(:year, :name, :foo, { sizes: [:name, :quantity] })
end

All you'd need to do is adjust the name of the sizes key in that method. I'd recommend:

def vintage_params
  vintage_params = params.require(:vintage).permit(:year, :name, :foo, { sizes: [:name, :quantity] })
  vintage_params[:sizes_attributes] = vintage_params.delete :sizes
  vintage_params.permit!
end

This will remove the :sizes key and put it in the expected :sizes_attributes without messing up your pretty json. There is nothing you can do directly with accepts_nested_attributes_for to change the name.

Sign up to request clarification or add additional context in comments.

4 Comments

"There is nothing you can do directly with accepts_nested_attributes_for to change the name." This is correct and unfortunate. I've had this same problem.
It doesn't work for me. Params are renamed correctly but when I try to execute Model.new(params) I get ActiveModel::ForbiddenAttributesError. It looks like there is some additional check before assigning attributes to the model instance.
You may need to do vintage_params.permit! as the last line in Rails 4.2 and later.
@ptd thanks for this solution! I wanted to mention an issue I ran into that others might as well. In the case where you send a request body that does not have the sizes param included in it, you may run into an issue with your model complaining that sizes_attributes can't be nil, e.g. if it's expecting an array. (Obviously this depends on your model.) I was able to solve this by just including a check to see if the param is provided: vintage_params[:sizes_attributes] = vintage_params.delete :sizes if vintage_params.key?(:sizes)
8

I too was looking for a way to avoid polluting my RESTful API with the nested attributes cruft. I thought I'd share my solution, as it's general enough to be useful for anyone running into the same issue. It begins with a simple module to be leveraged from your controller:

module PrettyApi
  class << self
    def with_nested_attributes(params, attrs)
      return if params.blank?

      case attrs
      when Hash
        with_nested_hash_attributes(params, attrs)
      when Array
        with_nested_array_attributes(params, attrs)
      when String, Symbol
        unless params[attrs].blank?
          params["#{attrs}_attributes"] = params.delete attrs
        end
      end
      params
    end

    private

    def with_nested_hash_attributes(params, attrs)
      attrs.each do |k, v|
        with_nested_attributes params[k], v
        with_nested_attributes params, k
      end
    end

    def with_nested_array_attributes(params, attrs)
      params.each do |np|
        attrs.each do |v|
          with_nested_attributes np, v
        end
      end
    end
  end
end

Here's an example of this module being used in a controller, used to upload Address Books from a mobile client:

class V1::AddressBooksController < V1::BaseController
  def create
    @address_book = AddressBook.new address_book_params
    unless @address_book.save
      errors = @address_book.errors.to_hash(true)
      render status: 422, json: { errors: errors }
    end
  end

  private

  def address_book_params
    PrettyApi.with_nested_attributes  pretty_address_book_params,
                                      contacts: [:emails, :phones, :addresses]
  end

  def pretty_address_book_params
    params.permit(
      :device_install_id,
      contacts: [
        :local_id,
        :first_name,
        :last_name,
        :nickname,
        emails: [
          :value,
          :type
        ],
        phones: [
          :value,
          :type
        ],
        addresses: [
          :type,
          :street_address,
          :city,
          :state,
          :postal_code,
          :country
        ]
      ]
    )
  end
end

Note, the syntax for declaring the nested attributes mirrors that of declaring permitted parameters in your controller.

Here's the Gist for this example.

I hope someone finds this helpful!

3 Comments

Worked for me, thanks. Wish you had put in some instructions as it wasn't self-explanatory how this worked.
All these solutions feel a little hacky for a framework that has been around this long. Surely in 2020 there is a best practice in terms of fetching nested json from the rails api, manipulating it slightly on the frontend and sending back to the controller...(without the need to append nested attributes) ?
@Branksy Five years after posting the above solution, I'm of the mind that nested attributes just shouldn't be used for REST/GraphQL APIs. As such, I don't believe this is the fault of the framework, rather it's the fault of developers such as myself for misusing this feature. These days, I'd advocate for sending JSON that clearly describes your data, and using service objects on the backend to figure out where and how to store it.

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.