Skip
Arish's avatar

23. Validations


Active Record Validations

Validations ensure that only valid data is saved to your database. They run automatically before save, create, and update.

When Validations Run

ruby
1# Validations are triggered by these methods:
2user.save
3user.save!
4user.create
5user.create!
6user.update(attributes)
7user.update!(attributes)
8user.valid?
9
10# These skip validations:
11user.save(validate: false)
12user.update_attribute(:name, "value")
13user.update_column(:name, "value")
14user.update_columns(name: "value")
15User.update_all(name: "value")

Common Validations

Presence

ruby
1class User < ApplicationRecord
2  validates :name, presence: true
3  validates :email, presence: { message: "is required" }
4  
5  # For boolean fields, use inclusion instead
6  validates :terms_accepted, inclusion: { in: [true] }
7end

Uniqueness

ruby
1class User < ApplicationRecord
2  validates :email, uniqueness: true
3  
4  # Case insensitive
5  validates :email, uniqueness: { case_sensitive: false }
6  
7  # Scoped uniqueness
8  validates :name, uniqueness: { scope: :organization_id }
9  
10  # Custom message
11  validates :username, uniqueness: { message: "is already taken" }
12end

Length

ruby
1class Article < ApplicationRecord
2  validates :title, length: { minimum: 5 }
3  validates :title, length: { maximum: 100 }
4  validates :title, length: { in: 5..100 }
5  validates :title, length: { is: 50 }  # Exact length
6  
7  # Custom messages
8  validates :bio, length: {
9    minimum: 10,
10    maximum: 500,
11    too_short: "must have at least %{count} characters",
12    too_long: "must have at most %{count} characters"
13  }
14end

Numericality

ruby
1class Product < ApplicationRecord
2  validates :price, numericality: true
3  validates :price, numericality: { only_integer: true }
4  validates :price, numericality: { greater_than: 0 }
5  validates :price, numericality: { greater_than_or_equal_to: 0 }
6  validates :price, numericality: { less_than: 1000 }
7  validates :price, numericality: { less_than_or_equal_to: 1000 }
8  validates :quantity, numericality: { odd: true }
9  validates :quantity, numericality: { even: true }
10end

Format

ruby
1class User < ApplicationRecord
2  validates :email, format: { 
3    with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i,
4    message: "must be a valid email address"
5  }
6  
7  validates :phone, format: { 
8    with: /\A\d{10}\z/,
9    message: "must be 10 digits"
10  }
11  
12  validates :username, format: {
13    without: /\s/,
14    message: "cannot contain spaces"
15  }
16end

Inclusion and Exclusion

ruby
1class User < ApplicationRecord
2  validates :role, inclusion: { 
3    in: %w[admin editor viewer],
4    message: "%{value} is not a valid role"
5  }
6  
7  validates :username, exclusion: { 
8    in: %w[admin superuser root],
9    message: "%{value} is reserved"
10  }
11end

Confirmation

ruby
1class User < ApplicationRecord
2  # User must provide email_confirmation field
3  validates :email, confirmation: true
4  validates :email_confirmation, presence: true
5  
6  # For passwords
7  validates :password, confirmation: true
8  validates :password_confirmation, presence: true
9end

Acceptance

ruby
1class User < ApplicationRecord
2  # For checkbox fields (terms of service)
3  validates :terms_of_service, acceptance: true
4  
5  # Custom acceptance value
6  validates :terms, acceptance: { accept: 'yes' }
7end

Conditional Validations

ruby
1class User < ApplicationRecord
2  # Only validate if condition is true
3  validates :phone, presence: true, if: :phone_required?
4  validates :company, presence: true, if: -> { role == 'business' }
5  
6  # Unless condition
7  validates :nickname, presence: true, unless: :formal_name?
8  
9  # Multiple conditions
10  validates :age, presence: true, if: [:adult?, :registration_complete?]
11  
12  private
13  
14  def phone_required?
15    role == 'admin'
16  end
17  
18  def formal_name?
19    title.present?
20  end
21end

On Create or Update

ruby
1class User < ApplicationRecord
2  validates :password, presence: true, on: :create
3  validates :password, length: { minimum: 8 }, on: :update
4  
5  # Custom context
6  validates :terms, acceptance: true, on: :checkout
7  
8  # Usage with custom context:
9  # user.save(context: :checkout)
10end

Custom Validations

Custom Method

ruby
1class Product < ApplicationRecord
2  validate :price_must_be_positive
3  validate :expiration_date_in_future, on: :create
4  
5  private
6  
7  def price_must_be_positive
8    if price.present? && price <= 0
9      errors.add(:price, "must be greater than zero")
10    end
11  end
12  
13  def expiration_date_in_future
14    if expiration_date.present? && expiration_date <= Date.today
15      errors.add(:expiration_date, "must be in the future")
16    end
17  end
18end

Custom Validator Class

ruby
1# app/validators/email_validator.rb
2class EmailValidator < ActiveModel::EachValidator
3  def validate_each(record, attribute, value)
4    unless value =~ /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
5      record.errors.add(attribute, options[:message] || "is not a valid email")
6    end
7  end
8end
9
10# Usage in model
11class User < ApplicationRecord
12  validates :email, email: true
13  validates :backup_email, email: { message: "must be valid" }
14end

Validates With

ruby
1class GoodnessValidator < ActiveModel::Validator
2  def validate(record)
3    if record.evil?
4      record.errors.add(:base, "This record is evil")
5    end
6  end
7end
8
9class User < ApplicationRecord
10  validates_with GoodnessValidator
11end

Working with Errors

ruby
1user = User.new(name: "", email: "invalid")
2user.valid?  # => false
3
4# Check errors
5user.errors.any?             # => true
6user.errors.empty?           # => false
7user.errors.count            # => 2
8
9# Get all error messages
10user.errors.full_messages    # => ["Name can't be blank", "Email is invalid"]
11user.errors.messages         # => { name: ["can't be blank"], email: ["is invalid"] }
12
13# Get errors for specific attribute
14user.errors[:name]           # => ["can't be blank"]
15user.errors[:email]          # => ["is invalid"]
16
17# Add custom error
18user.errors.add(:base, "Something is wrong")
19user.errors.add(:name, :blank)  # Uses i18n key
20
21# Clear errors
22user.errors.clear
23
24# Check specific attribute
25user.errors.include?(:name)  # => true
26user.errors.added?(:name, :blank)  # => true

Displaying Errors in Views

erb
1<%= form_with model: @user do |form| %>
2  <% if @user.errors.any? %>
3    <div class="error-summary">
4      <h2><%= pluralize(@user.errors.count, "error") %> prevented saving:</h2>
5      <ul>
6        <% @user.errors.full_messages.each do |message| %>
7          <li><%= message %></li>
8        <% end %>
9      </ul>
10    </div>
11  <% end %>
12  
13  <div class="field">
14    <%= form.label :name %>
15    <%= form.text_field :name %>
16    <% if @user.errors[:name].any? %>
17      <span class="error"><%= @user.errors[:name].first %></span>
18    <% end %>
19  </div>
20<% end %>

Combining Validations

ruby
1class User < ApplicationRecord
2  validates :name, 
3            presence: true, 
4            length: { minimum: 2, maximum: 50 }
5  
6  validates :email, 
7            presence: true, 
8            uniqueness: { case_sensitive: false },
9            format: { with: URI::MailTo::EMAIL_REGEXP }
10  
11  validates :password, 
12            presence: true, 
13            length: { minimum: 8 },
14            if: :password_required?
15  
16  validates :age, 
17            numericality: { only_integer: true, greater_than: 0 },
18            allow_nil: true
19            
20  validates :website, 
21            format: { with: URI::regexp(%w[http https]) },
22            allow_blank: true
23end

Allow Nil and Allow Blank

ruby
1class User < ApplicationRecord
2  # Skip validation if value is nil
3  validates :age, numericality: true, allow_nil: true
4  
5  # Skip validation if value is blank (nil, "", " ")
6  validates :website, format: { with: URI::regexp }, allow_blank: true
7end

Validations are your first line of defense for data integrity!