[Proposal] Optional class_names validation for polymorphic belongs_to association

Use case

In some project we introduce this kind of code to only allow some models in polymorphic associations

class Post < ActiveRecord::Base
  has_many :comments, inverse_of: :commented

class Article < ActiveRecord::Base
  has_many :comments, inverse_of: :commented

class Author < ActiveRecord::Base

class Comment < ActiveRecord::Base
  belongs_to :commented, polymorphic: true
  validates :commented_type, inclusion: { in: %w[Article Post] }

class PolymorphicValidationTest < Minitest::Test
  def test_comment_on_post_and_article
    post = Post.create!
    Comment.create! commented: post

    assert_equal 1, post.comments.count
    assert_equal 1, Comment.count
    assert_equal post.id, Comment.first.commented.id

  def test_comment_on_author_should_raise
    author = Author.create!
    assert_raises ActiveRecord::RecordInvalid do
      Comment.create!(commented: author)

There is multiple advantages to doing so

  • Explicit definition of what the polymorphic association accepts as linked models
  • We can used it when you need to introspect linked models

What do you think about introducing an optional class_names parameter to belongs_to to have something like that

belongs_to :commented, polymorphic: true, class_names: %w[Article Post]

This solution can handle coupling between the optional parameter and conditionaly run the validator if you have something like

belongs_to :commented, polymorphic: true, optional: true
validates :commented_type, inclusion: { in: %w[Article Post] }, if: -> { commented }

We already have the validation in some of our projects we have something like that for the moment

    def belongs_to_polymorphic(name, scope = nil, class_names:, **options)
      belongs_to name, scope, polymorphic: true, **options
      validates "#{name}_type".to_sym,
                inclusion: {
                  in: class_names,
                  message: "%{value} isn't accepted for this polymorphic relationship",
                unless: -> { options[:optional] && !send(name) }

I will dedicated some time to implement this in a PR if you think it’s a desirable feature for the core