Brandon Rice

Software development with a focus on web technologies.

Rails Validator Classes and Instance Vars

| Comments

Have you ever used a custom validator class in Rails? I’m referring to a subclass of ActiveModel::Validator which you then include inside your model such as…

1
2
3
4
5
6
7
class MyModel < ActiveRecord::Base

  include ActiveModel::Validations
  validates_with MyValidator

  # ...etc.
end

This allows you to define your validation methods in a custom class. It can be useful for extracting validation behavior out of your model. However, what you might not know is how this class is instantiated by Rails. I assumed that a new instance of my validator would be created each time validation was performed on an instance of the model during a web request. I also assumed that the validator instance would stick around if multiple validation attempts were made. Based on this assumption, I attempted to memoize the model instance inside my validator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyValidator < ActiveModel::Validator
  attr_reader :model

  def validate(record)
    @model ||= record
    a_validation_method
    another_validation_method
  end

  private

  def a_validation_method
    if !check_for_validity
      model.errors[:base] << 'No good'
    end
  end

  def another_validation_method
    # etc...
  end
end

This won’t work, and leads to some unexpected behavior. Everything seemed fine while submitting my form via the browser, but my tests were failing. Things that should have been valid weren’t, and vice versa. This eventually led me to do a little research on how Rails uses validators.

ActiveModel::Validations::ClassMethods#validates_with
1
2
3
4
5
6
7
8
9
10
11
def validates_with(*args, &block)
  options = args.extract_options!
  args.each do |klass|
    validator = klass.new(options, &block)
    validator.setup(self) if validator.respond_to?(:setup)

    # more stuff...

    validate(validator, options)
  end
end

The moral of the story is that validates_with is a class method that creates a single instance of the validator when the model class is first loaded. If you memoize an instance variable inside the validator, it will not be replaced on successive calls to the validate method. In other words, the validator might be trying to validate the wrong object.

If you enjoyed this post, please consider subscribing.

Comments