Search code examples
ruby-on-railsactiverecord

Why is Rails numericality validator not using normalized value?


My model has a decimal amount attribute.

create_table :foos do |t|
  t.decimal :amount
end

class Foo < ApplicationRecord
end

I always want the amount to be negative, so I add a normalisation:

class Foo < ApplicationRecord
  normalizes :amount, with: -> amount { - amount.abs }
end

This seems to work perfectly.

Now, to be safe, I add a validation:

class Foo < ApplicationRecord
  normalizes :amount, with: -> amount { - amount.abs }
  validates :amount, numericality: {less_than: 0}
end

Now when I set the amount to a positive value, although the normalisation converts it to a negative value, the validator seems to think the value is still positive and adds a validation error.

foo = Foo.new amount: 4
foo.amount  # => -4
foo.valid?  # => false
foo.errors  # => #<ActiveModel::Error attribute=amount, type=less_than, options={:value=>4, :count=>0}>

According to the tests for normalizes, normalisation happens before validation.

How can I get this to work?


Solution

  • Numericality validator seems to be specifically using raw value for validation without taking normalization into account:
    https://github.com/rails/rails/blob/v7.1.3/activemodel/lib/active_model/validations/numericality.rb#L129

    if record.respond_to?(came_from_user)
      if record.public_send(came_from_user)
        raw_value = record.public_send(:"#{attr_name}_before_type_cast")
    

    It needs to be this way because strings normalize to numbers ("foo".to_d # => 0.0) so validation wouldn't work if it happened after normalization.

    You could write your own validation to bypass this problem:

    validate do
      errors.add(:amount, :less_than, value: amount, count: 0) unless amount.negative?
    end