Custom validators and validation patterns

Sarah Mitchell Feb 2026
3 tabs
# app/validators/order_validator.rb
class OrderValidator < ActiveModel::Validator
  def validate(record)
    validate_order_total(record)
    validate_items_availability(record)
    validate_shipping_address(record)
  end

  private

  def validate_order_total(record)
    calculated_total = record.items.sum(&:price)

    if record.total != calculated_total
      record.errors.add(:total, 'does not match item prices')
    end
  end

  def validate_items_availability(record)
    record.items.each do |item|
      unless item.in_stock?
        record.errors.add(:base, "Item #{item.name} is out of stock")
      end
    end
  end

  def validate_shipping_address(record)
    if record.requires_shipping? && record.shipping_address.blank?
      record.errors.add(:shipping_address, 'is required for physical items')
    end
  end
end

class Order < ApplicationRecord
  has_many :items
  belongs_to :shipping_address, optional: true

  validates_with OrderValidator
end
3 files · ruby Explain with highlit

Rails validations ensure data integrity before persistence. I create custom validators for complex business rules. Validators inherit from ActiveModel::Validator or ActiveModel::EachValidator. EachValidator validates individual attributes; Validator validates entire records. Custom validators accept options for configurability. I combine validations—presence, format, uniqueness, custom logic. Conditional validations use :if and :unless. validate method adds custom validation logic inline. Validation errors attach to specific attributes or base. valid? triggers validations; invalid? checks validity. Custom validators encapsulate reusable validation logic, keeping models clean. Proper validation prevents bad data, reduces bugs, and improves user feedback.