accepts_nested_attributes_for with Has-Many-Through Relations

Pat Brisbin

If you find yourself getting validation errors when using accepts_nested_attributes_for with has-many-through relations, the answer may be to add an inverse_of option.

The inverse_of option allows you to tell Rails when two model relations describe the same relationship, but from opposite directions. For example, if a User has_many :posts and a Post belongs_to :user, you can tell Rails that the :user relation on Post is the inverse of the :posts relation on User.

This option is usually not required, but there are cases where it matters. One such case is when using accepts_nested_attributes_for with a has-many-through relation. This will eventually lead to a collection= assignment which is only possible if Rails knows that one relation is the inverse of another.


In our case, we had the following three models:

class Notice < ActiveRecord::Base

  # attribute :title

  has_many :entity_roles
  has_many :entities, through: :entity_roles

  accepts_nested_attributes_for :entity_roles


class EntityRole < ActiveRecord::Base

  # attribute :name

  belongs_to :entity
  belongs_to :notice

  validates_presence_of :entity
  validates_presence_of :notice

  accepts_nested_attributes_for :entity


class Entity < ActiveRecord::Base

  # attribute :name
  # attribute :address

  has_many :entity_roles
  has_many :notices, through: :entity_roles


We wanted the form to create two related entities for the notice, each of a specific role.

The controller looks like this:

class NoticesController < ApplicationController

  def new
    @notice = 'submitter').build_entity 'recipient').build_entity


Using Simple Form, the view looks like this:

<%= simple_form_for(@notice) do |form| %>
  <%= form.input :title %>

  <%= form.simple_fields_for(:entity_roles) do |roles_form| %>
    <% role = %>
    <%= roles_form.input :name, as: :hidden %>
    <%= roles_form.simple_fields_for(:entity) do |entity_form| %>
      <%= entity_form.input :name, label: "#{role} Name" %>
      <%= entity_form.input :address, label: "#{role} Address" %>
    <% end %>
  <% end %>

  <%= form.submit "Submit" %>
<% end %>

The only clever bit here is that we use each role’s name to intelligently affect the entity form’s labels each time it’s rendered. Aside from that, it’s pretty standard accepts_nested_attributes stuff.

On POST, we found validation errors on the entity_role objects:

["notice", "can't be blank"]

We were confused.

The controller’s #create action is effectively doing this:

notice =
  title: "...",
  entity_roles_attributes: [
    { name: "submitter", entity_attributes: { ... } },
    { name: "recipient", entity_attributes: { ... } }

Which, as far as we knew, should work.

It seemed Rails was not setting the notice attribute on the EntityRole before attempting to save it, triggering the validation errors. This is a bit surprising as other has_many relations (omitted in this blog post) should have the same save mechanics and were working just fine.

In an act of experimentation, we added inverse_of:

class Notice < ActiveRecordBase

  has_many :entity_roles, inverse_of: :notice


And suddenly, it all worked.

Only after the fact, when we knew to include “inverse_of” in our search queries, did we find some information on this issue. You can read the details here, here, and here if you're interested.

When you use collection= assignment with a has-many-through (as accepts_nested_attributes_for does), you have to specify inverse_of for Rails to save everything correctly.