Rails Validations Deep — Advanced Validation Techniques
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
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