Hedronite Lesson · Polyglot-Dev / Rails · Mon 2026-09-14

Active Record Validations

A validation is a model-layer gate: run before INSERT or UPDATE, refuse the write when invalid, leave reasons on errors.

Lesson Class: Duha (Rails Guides track)
Focus: validates · valid?/errors · triggers vs skips · presence · uniqueness+index
Code Blocks: clean blocks, explanation in prose
Done-criteria: declare validations, valid?/errors, trigger vs skip, uniqueness+index
Grounding: rails-guides/active_record_validations.html · VERSION.txt v8.1.3.1 · Flanagan not cited
The gate
Model validations refuse INSERT/UPDATE when the object is invalid.
The ledger
errors collects per-attribute messages after valid? or a failed save.
The race
uniqueness in the model still wants a unique index in the database.
A validation is a model-layer gate: run before INSERT or UPDATE, refuse the write when invalid, leave reasons on errors.

<!-- hal:authoritative:yaml -->

*A validation is a model-layer gate: run before INSERT or UPDATE, refuse the write when the object is invalid, and leave the reasons on errors for the caller to read.*

§I — Frame

Duha session 04 for the Rails track. Dual-fire beside Rust. The spine remains the official Rails Guides snapshot on disk: rails-guides/ at v8.1.3.1. Today's file is active_record_validations.html.

Session 03 versioned the schema. Session 02 mapped models to tables and ran CRUD. Those saves assumed the attributes were worth keeping. This fire owns the rules that decide whether a save should proceed.

By the end, declare validates on a model, call valid? and read errors, know which APIs trigger validations and which skip them, and state why uniqueness in the model still wants a unique index in the database. Callbacks, associations depth, and the query interface stay later syllabus rows.

§II — Why model validations

Validations keep invalid data out of the database. They work with any database Rails talks to, cannot be turned off by an end-user form, and stay easy to test.

The guide names three alternate layers and why they are complements, not replacements:

  1. Database constraints — strong for uniqueness under concurrency; database-dependent and harder to unit-test alone.
  2. Client-side checks — fast feedback; bypassable if JavaScript is off.
  3. Controller checks — temptingly local; become unwieldy and hard to maintain.

Rails recommends model-level validations in most circumstances. Pair them with database constraints when the failure mode is a race between two writers.

The smallest form:

class Person < ApplicationRecord
  validates :name, presence: true
end
Person.new(name: "John Doe").valid?  # => true
Person.new(name: nil).valid?         # => false

§III — When validations run (and when they do not)

Active Record runs validations before the SQL INSERT or UPDATE that would persist the object. If any validation fails, the object is invalid and the write does not go out.

Methods that trigger validations (and only persist when valid) include:

Bang forms raise when the object is invalid. Non-bang forms return false (or nil for some create paths) and leave messages on errors.

Methods that skip validations exist and will write anyway. Treat them as sharp tools: update_attribute, update_column, update_columns, update_all, insert / insert_all, upsert / upsert_all, counters, touch, and save(validate: false). If you call them, you own the bypass.

new alone does not run validations. An object can look clean until you save, create, or call valid? yourself.

§IV — valid? and errors

valid? runs the validations and returns true when the errors collection is empty afterward. invalid? is the inverse.

person = Person.new
person.errors[:name]       # => []  — validations not run yet
person.valid?              # => false
person.errors[:name]       # => ["can't be blank"]

errors[:attribute] returns the messages for that attribute. errors.add lets custom logic push a message. Dig further with the guide’s Working with Validation Errors section (errors.where, errors[:base], errors.size, errors.clear) when you need more than a boolean.

Views can render those messages; the HTML helpers are a later Forms / Layouts concern. Today own the model collection.

§V — Built-in helpers you will use first

Declare helpers with validates (preferred) rather than the older validates_*_of forms. Common ones from this Guides page:

presence — attribute is not blank? (nil or whitespace-only string).

class Person < ApplicationRecord
  validates :name, :login, :email, presence: true
end

For associations, validate the associated object, not only the foreign key, when you need the referent to exist.

uniqueness — no other row with the same value at save time (SQL lookup). Optional :scope narrows the check:

class Account < ApplicationRecord
  validates :email, uniqueness: true
end

class Holiday < ApplicationRecord
  validates :name, uniqueness: { scope: :year, message: "should happen once per year" }
end

Model uniqueness is not a database constraint. Two connections can still race. Add a unique index in a migration (add_index ..., unique: true) when the column must stay unique under load. Session 03’s migration DSL is how that index lands.

length — :minimum, :maximum, :in, :is.

numericality — numeric values; :only_integer when floats are wrong.

format — match or reject a regexp (:with / :without). Prefer \A and \z over ^ and $ unless you pass multiline: true on purpose.

Also on the page: absence, acceptance, confirmation, comparison, inclusion / exclusion, validates_associated, validates_each, validates_with. Learn the map; drill the ones your models actually need.

Shared options across many validators: :allow_nil, :allow_blank, :message, :on (:create / :update / custom contexts), and :strict (raise on failure).

§VI — Conditional validations

Validate only when a condition holds with :if / :unless (symbol, Proc, or array):

class Order < ApplicationRecord
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

Group conditions when several validations share a gate. Custom contexts via :on let you run a named subset with valid?(:account_setup) or save(context: :account_setup) without inventing a second model.

Custom validators and custom methods (validate :method_name) extend the built-ins when a helper does not name the rule. Keep that path short today; the guide’s Custom Validations section is the map when a helper runs out.

§VII — One complete proof

  1. Add validates :name, presence: true and validates :email, uniqueness: true (or equivalent) on a model you already have.
  2. In console: build an invalid record, call valid?, read errors[:name] / errors[:email], then fix attributes and save.
  3. Name one method that skips validations and say why you would not use it for ordinary form input.
  4. Say aloud: model uniqueness still wants a unique index if two writers can race.

When those four hold, this Guides page’s selected depth is done.

§VIII — Closing

Validations are the model’s refusal to persist a bad object. Prefer them over controller-only checks. Know the trigger list and the skip list. Read errors after valid?. Start with presence and uniqueness, then reach for length, numericality, and format as the attributes demand. Conditionals keep rules honest to the workflow. Session 03’s migrations remain the place to put the unique index that backs uniqueness under concurrency.

Done-criteria: declare validations, check valid?/errors, name a trigger vs skip method, and state the uniqueness+index pairing.

Related