Validation definitions: class level vs object level

Hi,

I was wondering if adding validations to the ActiveModel or ActiveRecord instances would be possible? Composing the extra validations at the object level instead of adding more code to a model definition.

I know about contexts and can see how a Decorator could validate a record before an event like saving but that becomes a 2-step validation.

It would work like context but injected into an object instead of being defined in the class. Something like this:

class AgeValidator < ActiveModel::Validator
  def validate(record)
    record.errors.add :age, :blank unless record.age
  end
end

class Person < ActiveRecord::Base
  attr_accessor :name, :age

  validates :name, presence: true
end

person1 = Person.new
person1.validates_with AgeValidator
person1.save
puts person1.errors.full_messages
# => ["Age can't be blank", "Name can't be blank"]

person2 = Person.new
person2.save
puts person2.errors.full_messages
# => ["Name can't be blank"]

I saw that ActiveModel::Validations#validates_with is already defined. It can be another name I thought this one was a good one. The code example isn’t the current behaviour as #save clears errors and rerun validation defined in the class only.

I tried to mess with _validators on the object but the parent class gets updated as well instead of being contained to the object. I’m not sure if my understanding is correct on validation callbacks but it looks like these are shared between classes and their instances. If this is true is it for performance reasons?

Would it be possible that each instance gets its own validators/callbacks, duplicated from the class instead of using the one from the class?

Or maybe have another level of validators/callbacks that get populated on an object and run after the class validation callbacks are run.

Is that something possible or considered a feature request?

There are multiple approaches you can take to tackle this.

  1. Conditional validations. If there is a clear definition of condition when you want and when you don’t want to validate the age, you can pass it as :if or :unless argument to the validator.

  2. class PersonWithAge < Person. Just define an extra class and add your validations to that class.

  3. Custom validator method. You don’t have to call it on each validation, you can call it manually:

class Person < ActiveRecord::Base
  attr_accessor :name, :age

  validates :name, presence: true

  def validate_age
    record.errors.add :age, :blank unless record.age
  end
end

person1 = Person.new
person1.validate_age
person1.save
puts person1.errors.full_messages
# => ["Age can't be blank", "Name can't be blank"]

person2 = Person.new
person2.save
puts person2.errors.full_messages
# => ["Name can't be blank"]
  1. You were actually pretty close. :wink:
person1.singleton_class.validates_with ActiveRecord::Validations::PresenceValidator, attributes: :age
# or simply
person1.singleton_class.validates_presence_of :age

This will add this validator to the _validate_callbacks stack for that one object only.

That’s just from top of my head, there could be more ways. TIMTOWTDI!

PS. There’s also option 5, monkeypatching the save or validate method for that one object, but I would advise against going that way (unless it’s a pet project and you’re the only developer on it). Doesn’t mean you should not do it, just be aware of side effects and consequences.