Brian Weinreich
Brian Weinreich

Reputation: 7702

How to accept JSON through Rails API without "_attributes" for nested fields

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?

Upvotes: 24

Views: 6358

Answers (2)

ptd
ptd

Reputation: 3053

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.

Upvotes: 32

Tim Lowrimore
Tim Lowrimore

Reputation: 2052

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!

Upvotes: 8

Related Questions