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.

author image
Pat Brisbin

Pair with one of our expert developers to level up your skills with Coaching by thoughtbot. Save time learning best practices and techniques for reducing technical debt in Ember, Ruby, Haskell, and Go in 1-on-1 sessions tailored to your goals.