Building Custom Validators in Rails

The basic ActiveRecord validations are well-known to most Rails developers.

class Person < ActiveRecord::Base
  validates :name, presence: true
end

Person.new(name: nil).valid? # false

If there is no built-in validator that fits your needs you can always use a custom method to perform the validation.

class Image < ActiveRecord::Base
  validate :valid_file_name_extension

  private

  def valid_file_name_extension
    if file_name.present? && !%w{.jpg .png .jpeg .tif}.include?(File.extname(file_name.downcase))
      errors.add(:file_name, :invalid_extension)
    end
  end
end

This approach becomes troublesome if you need to apply the same validation to multiple attributes or models. This is where we can use a custom validator.

Custom Validators

Custom validators usually inherit from the ActiveModel::EachValidator class and define a validate_each method. I usually put all validators in /app/validators.

# app/validators/image_file_name_validator.rb
class ImageFileNameValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
  end
end

The syntax for using our custom validator is the same as for the built-in validators.

class Image < ActiveRecord::Base
  validates :file_name, image_file_name: { supported_extensions: %w{.jpg .png .jpeg .tif} }
end

The validate_each method is provided with the model instance, the name of attribute being validated and the value of the attribute. We also have access to the slightly confusing options hash which contains optional parameters passed to the validator. In this case it would contain { supported_extensions: %w{.jpg .png .jpeg .tif} }.

Implementing the logic for the validator is now pretty straightforward.

class ImageFileNameValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    supported_extensions = options.fetch(:supported_extensions, %w{.jpg .png .jpeg})
    if value.present? && !supported_extensions.include?(File.extname(value.downcase))
      record.errors.add(:file_name, :invalid_extension, valid_extensions: supported_extensions)
    end
  end
end

By default this code will validate that the extension is either .jpg, .png or .jpeg - but allows this list of valid extensions to be customized as we’re doing in our example.

Note that I’m passing a symbol - :invalid_extension - to the errors object, instead of specifying an error message. This means we can specify the actual error message in our locales file instead of hardcoding the error message. I am also supplying the valid extensions as additional details that can be used by the error message.

Rails also allows us to easily test custom validators, which will also help to illustrate how our validator behaves.

Of course, when testing a validator we need some kind of model to test against. Instead of coupling our validator to an existing model it would be ideal if we could generate some kind of stub model. While we can simply write a class in our test - see this approach for an example - I prefer to use the with_model gem to do the boilerplate for me. I’m also using the shoulda-matchers gem that allows for succinct syntax.

RSpec.describe ImageFileNameValidator do
  context 'when supported extensions are supplied' do
    with_model :ObjectWithFileName do
      model do
        attr_accessor :file_name
        validates :file_name, image_file_name: { supported_extensions: %{.jpg .jpeg} }
      end
    end

    let(:model) { ObjectWithFileName.new }

    it 'allows nil values' do
      expect(model).to allow_value(nil).for(:file_name)
    end

    it 'allows values with the specified extensions' do
      expect(model).to allow_value('thumbnail.jpeg').for(:file_name)
      expect(model).not_to allow_value('thumbnail.png').for(:file_name)
    end
  end

  context 'when no supported extensions are specified' do
    with_model :ObjectWithFileName do
      model do
        attr_accessor :file_name
        validates :file_name, image_file_name: true
      end
    end

    let(:model) { ObjectWithFileName.new }

    it 'allows nil values' do
      expect(model).to allow_value(nil).for(:file_name)
    end

    it 'allows jpg, png and jpeg extensions' do
      expect(model).to allow_value('thumbnail.jpg').for(:file_name)
      expect(model).to allow_value('thumbnail.png').for(:file_name)
      expect(model).to allow_value('thumbnail.jpeg').for(:file_name)
      expect(model).not_to allow_value('thumbnail.tif').for(:file_name)
    end
  end
end

Extending Built-in Validators

Many of the built-in validations allow us to specify many different options to get the exact validation behavior we want. For example, if we are storing the square footage of a property we would want to validate that the attribute is a positive integer.

class Property < ActiveRecord::Base
  validates :size, greater_than: 0, only_integer: true, allow_nil: true
end

However, if we need the same validation on multiple attributes or models it becomes troublesome to duplicate this validation. For core application logic it makes sense to wrap this in a custom validator that simply captures the custom options we pass to the built-in validator.

class SquareFootageValidator < ActiveModel::Validations::NumericalityValidator
  def initialize(options = {})
    super(options.reverse_merge(greater_than: 0, only_integer: true, allow_nil: true))
  end
end

We can use the same pattern to test our validator.

RSpec.describe SquareFootageValidator do
  with_model :ObjectWithSizeFootage do
    model do
      attr_accessor :size
      validates :size, square_footage: true
    end
  end

  let(:model) { ObjectWithSizeFootage.new }

  it 'allows integer values greater than zero' do
    expect(model).to allow_value(1).for(:size)
    expect(model).to allow_value(50).for(:size)
    expect(model).not_to allow_value(0).for(:size)
    expect(model).not_to allow_value(-50).for(:size)
  end

  it 'allows nil values' do
    expect(model).to allow_value(nil).for(:size)
  end
end