`belongs_to` relationships do not show up as required on rails 7.1+

Author: siegfaultCreated Aug 15, 2024Updated Feb 7, 2026

Environment

  • Ruby 3.3.0
  • Rails 7.2.0
  • Simple Form 5.3.1

Current behavior

By default, on rails 7.1+, belongs_to relationships do not show up as required.

Sample App
ruby
# frozen_string_literal: true

require 'bundler/inline'

gemfile(true) do
  source 'https://rubygems.org'

  gem 'rails'
  gem 'simple_form'
  gem 'sqlite3'
end

require 'minitest/autorun'
require 'active_record'
require 'action_view'

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new($stdout)

ActiveRecord::Schema.define do
  create_table :users, force: true
  create_table :payments, force: true do |t|
    t.references :user
  end
end

class User < ActiveRecord::Base
end

class Payment < ActiveRecord::Base
  belongs_to :user
end

class RequiredTest < ActionView::TestCase
  def test_required
    render inline: <<~ERB
      <%= simple_form_for Payment.new, url: '' do |f| %>
        <div>
          <%= f.association :user %>
        </div>
      <% end %>
    ERB

    element = rendered.html.at('div')

    assert_match /\*/, element.text
  end
end

This works if we add a redundant validates :user, presence: true.

Expected behavior

belongs_to relationships should show up as required

Details

This is due to an interaction between how we calculate required, specifically at https://github.com/heartcombo/simple_form/blob/c2c7faf3e532072e1ef511a97d677ae0e7c38957/lib/simple_form/helpers/validators.rb#L24, and a new change in rails 7.1 (https://github.com/rails/rails/blob/9f9deaf6c14e3fdb3134261a9d52c57208c750ee/activerecord/lib/active_record/associations/builder/belongs_to.rb#L127-L140 / https://github.com/rails/rails/pull/46522) to conditionally define the validation.

In the short term, we can fall back to the old behavior by setting config.active_record.belongs_to_required_validates_foreign_key to true (thus unconditionally defining the validation), but my understanding is that this is a short-lived config to support backwards compatibility, and besides, the new behavior is a performance improvement.

Assuming y'all agree this is something that should be fixed (rather than, say, defining the redundant validation, passing required: true to the field in the form, or something else), I'd be happy to work on a patch if I could get some guidance. Something like

diff
-  validator.options.include?(:if) || validator.options.include?(:unless) 
+ if validator.options.include?(:if)
+   file_path, line_number = validator.options[:if].source_location
+   !file_path.match?(%r{lib/active_record/associations/builder/belongs_to.rb}) && line_number != 130
+ else
+   validator.options.include?(:unless)
+ end

would obviously work but be incredibly hacky. How would we want to approach a more robust way to allow this particular conditional validation where the item should always be present?