[Feature Proposal] Raise error on absent attribute during validation

Summary

Take the following:

class Person < ApplicationRecord
  validate :custom_validation

  private

  def custom_validation
    errors.add(:i_dont_exist, :presence)
  end
end

Then:

person = Person.new

person.valid?

The problem

When valid? is called, methods are executed down to generate_message, which is called on the Error class to build the validation messages.

generate_message tries to fetch the attribute value for the attribute key provided to errors.add by calling read_attribute_for_validation on the base object being validated in case the attribute key passed is not :base.

activemodel/lib/active_model/error.rb

module ActiveModel
  ...
  class Error
    ...
    def self.generate_message(attribute, type, base, options) # :nodoc:
      ...
      value = (attribute != :base ? base.read_attribute_for_validation(attribute) : nil)
      ...
    end
    ...
  end
end

The read_attribute_for_validation is currently just an alias to send.

activemodel/lib/active_model/validations.rb

module ActiveModel
  module Validations
    extend ActiveSupport::Concern
    ...
    module ClassMethods
        ...
	alias :read_attribute_for_validation :send
    end   
  end
end

Currently if a non-existent attribute key is passed to errors.add, a NoMethodError is raised during the generate_message call (raised by send under the hood), which does not hint at the fact that there’s a validation being run for an attribute that is not there.

The proposal

By overriding read_attribute_for_validation as follows and checking if the object being validated does not respond to a given attribute being validated it’s possible to raise an error that actually points the developer to what’s going on.

activemodel/lib/active_model/validations.rb
module ActiveModel
  module Validations
    extend ActiveSupport::Concern
    ...
    module ClassMethods
        ...
      def read_attribute_for_validation(attr)
        raise AbsentAttributeError.new(self, attr) unless self.respond_to?(attr)
        send(attr)
      end
    end   
  end
  ...
  class AbsentAttributeError < StandardError
    attr_reader :record, :attribute

    def initialize(record, attribute)
      @attribute = attribute
      super("Attribute #{attribute} does not exist for #{record.class}.")
    end
  end
end

By the way I created a PR for this. https://github.com/rails/rails/pull/40528

Ps: I tried editing the original post but couldn’t, so I’m posting it as a reply.

Thanks.