Skip to content

Rails Validations Deep — Advanced Validation Techniques

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Rails Validations Deep. We cover key concepts, practical examples, and best practices to help you master this topic.

Rails validations ensure data integrity with built-in helpers, custom validators, conditional validations, validation contexts, custom error messages, and I18n support.

What You'll Learn

By the end of this tutorial, you'll write custom validators, use conditional validations with procs and methods, implement validation contexts, customize error messages, and validate associations.

Why Validations Matter

Validations are the first line of defense against bad data. They provide user-friendly error messages and prevent invalid records from persisting.

Real-World Use

A registration form validates email format, password complexity, and age. Custom validators check domain-specific rules. Conditional validations apply only for admin-created users.

Validations Path

flowchart LR
  A[ActiveRecord] --> B[Validations Deep]
  B --> C[Custom Validators]
  B --> D[Conditional]
  B --> E[Contexts]
  B --> F[Errors API]
  B --> G{You Are Here}
  style G fill:#f90,color:#fff

Custom Validators

Create reusable validator classes.

# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
      record.errors.add(attribute, options[:message] || "is not a valid email address")
    end

    domain = value.split("@").last
    if options[:check_mx] && !domain_has_mx_record?(domain)
      record.errors.add(attribute, "domain does not accept email")
    end
  end

  private

  def domain_has_mx_record?(domain)
    Resolv::DNS.open { |dns| dns.getresources(domain, Resolv::DNS::Resource::IN::MX).any? }
  end
end

# Usage
class User < ApplicationRecord
  validates :email, email: { check_mx: true }
end

Conditional Validations

Apply validations based on conditions.

class Order < ApplicationRecord
  # Using symbol
  validates :shipping_address, presence: true, if: :requires_shipping?
  validates :card_number, presence: true, unless: :paid_with_store_credit?

  # Using Proc
  validates :gift_message, length: { maximum: 500 }, if: -> { gift_wrap?
  }
  validates :coupon_code, presence: true, on: :checkout

  # Multiple conditions
  with_options if: :paid_with_credit_card? do |order|
    order.validates :card_number, presence: true
    order.validates :expiry_date, presence: true
    order.validates :cvv, presence: true
  end

  private

  def requires_shipping?
    !digital_only? && !pickup?
  end
end

Validation Contexts

Run validations in specific contexts.

class User < ApplicationRecord
  validates :terms_accepted, acceptance: true, on: :registration
  validates :email, presence: true
  validates :password, length: { minimum: 8 }, on: [:registration, :password_change]
  validates :bio, length: { maximum: 500 }, on: :profile_update
end

# Usage
user.save(context: :registration)           # Runs :registration validations
user.save(context: :profile_update)          # Runs :profile_update validations
user.save                                    # Runs default validations
user.valid?(:registration)                   # Check only :registration validations

Errors API

Work with validation errors.

user = User.new(email: "invalid")
user.valid?
user.errors.full_messages        # ["Email is invalid"]
user.errors.count                # 1
user.errors.where(:email)        # Array of errors on email
user.errors[:email]              # ["is invalid"]

# Add custom errors
def validate
  if some_condition
    errors.add(:base, "Cannot save this record")
    errors.add(:email, :invalid, message: "is not properly formatted")
    errors.add(:password, :too_short, count: 8)
  end
end

# Clear errors
user.errors.clear
user.errors.delete(:email)

Custom Validation Methods

Add validations via custom methods.

class Invoice < ApplicationRecord
  validate :total_matches_items
  validate :payment_terms, on: :submit

  private

  def total_matches_items
    calculated_total = line_items.sum { |item| item.quantity * item.unit_price }
    if total != calculated_total
      errors.add(:total, "does not match line items total (#{calculated_total})")
    end
  end

  def payment_terms
    if due_date < Date.today
      errors.add(:due_date, "cannot be in the past")
    end
  end
end

Common Mistakes

1. Validations Without Database Constraints

Validations can be bypassed. Add database constraints for critical rules.

2. Over-Validating on Update with Presence

Presence validations on fields that should not change. Use on: :create.

3. Complex Validation Logic in Models

Complex validation logic belongs in service objects or custom validators.

4. Not Using I18n for Error Messages

Hardcoded error messages cannot be translated. Use I18n for user-facing messages.

5. Validating Uniqueness Without Database Index

Uniqueness validation is not race-condition safe without a unique index.

Practice Questions

1. What is a custom validator?

A class inheriting from ActiveModel::EachValidator with validate_each method.

2. How do you run validations only on update?

validates :field, presence: true, on: :update.

3. What does errors.add(:base, "message") do?

Adds a general error message not tied to a specific attribute.

4. How do you create a conditional validation?

Use if: or unless: with a symbol method name or a Proc.

5. Challenge: Create a custom validator for a coupon code format.

class CouponValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A[A-Z0-9]{4,12}\z/
      record.errors.add(attribute, "must be 4-12 uppercase alphanumeric characters")
    end
    if Coupon.expired.any? { |c| c.code == value }
      record.errors.add(attribute, "has expired")
    end
  end
end

FAQ

What is the difference between validates and validate?

validates uses validator classes. validate calls a custom method.

Can I skip validations?

Yes. Use save(validate: false) or update_attribute.

What is validates_associated?

Validates the associated records when the parent is saved.

Does validates :uniqueness prevent duplicates?

It checks at the application level. Add a database unique index for safety.

How do I customize error messages?

Use :message option, I18n yaml files, or custom validators.

Mini Project: User Registration with Validations

Build a user registration model with comprehensive validations.

class User < ApplicationRecord
  validates :email, presence: true, email: true, uniqueness: true
  validates :password, length: { minimum: 8, maximum: 72 }, if: :password_required?
  validates :username, length: { in: 3..30 }, uniqueness: { case_sensitive: false }
  validates :age, numericality: { greater_than_or_equal_to: 13, less_than: 150 }, on: :registration
  validates :terms, acceptance: true, on: :registration

  private

  def password_required?
    new_record? || password.present?
  end
end

What's Next

Rails Associations Deep Rails Forms Deep Rails Migrations Deep

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro