DEV Community

Cover image for validates vs validate in Rails
Hassan Farooq
Hassan Farooq

Posted on

validates vs validate in Rails

The difference between validates and validate in Rails is one letter, and that letter changes everything. One is the built-in helper you feed a list of rules. The other is a hook for a method you write yourself. People mix them up because they read almost the same out loud, so the way I keep them straight is to remember that the plural one comes with rules included and the singular one is bring-your-own.

validates: the built-in rules

validates, with the s, takes an attribute and a set of standard rules that Rails already knows how to enforce. Presence, uniqueness, length, numericality, format, and so on.

class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true
  validates :age,   numericality: { greater_than: 0 }
  validates :title, length: { maximum: 255 }
end
Enter fullscreen mode Exit fullscreen mode

You're not writing any logic here. You're declaring what should be true and letting Rails supply the checking. This covers the large majority of everyday validations, and if a built-in rule fits, use it instead of hand-rolling anything.

validate: your own method

validate, no s, registers a method you wrote. You reach for it when the rule is specific to your domain or depends on more than one field, so no built-in helper covers it. The classic example is making sure an event doesn't end before it starts.

class Event < ApplicationRecord
  validate :end_after_start

  private

  def end_after_start
    return if starts_at.blank? || ends_at.blank?

    if ends_at <= starts_at
      errors.add(:ends_at, "must be after the start time")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Three details in that method matter, and they're the parts people get wrong the first time.

The guard clause comes first. I return early if either timestamp is missing, because whether the fields are present is a different question. That's a job for validates :starts_at, :ends_at, presence: true. My custom method only cares about the order of two values that already exist. Skip the guard and you get a nil comparison blowing up, plus a confusing second error on top of the presence one.

errors.add is how a custom validation fails. You attach a message to a specific attribute, and that's what flips valid? to false and what shows up in event.errors for the form to display. No errors.add, no failure, the record saves happily.

The method is private. Nothing outside the model should be calling end_after_start directly, so it lives below a private line with the rest of the internals.

Both of these stop at the application layer

Here's the part worth saying out loud in an interview. validates and validate both run in Ruby, before the INSERT or UPDATE. They're perfect for friendly error messages on a form. They do not protect you under concurrency.

Picture two requests signing up with the same email at the same instant. Both run the uniqueness check, both see no existing row, both pass, both insert. Now you have duplicate emails and a validation that swore it prevented exactly that. The fix is a database constraint underneath the validation:

add_index :users, :email, unique: true
Enter fullscreen mode Exit fullscreen mode

The pattern is validation for the user experience, constraint for correctness. For the event example you can go a step further and add a Postgres check so the rule holds even if a row gets written outside the Rails model:

add_check_constraint :events, "ends_at > starts_at", name: "events_end_after_start"
Enter fullscreen mode Exit fullscreen mode

The short version

validates with an s is for Rails' built-in rules you declare. validate with no s is for a custom method you write, where the guard clause and errors.add do the work. And neither one is a substitute for a database constraint when the data absolutely has to be correct. Use the validation so the user gets a clear message, and back it with a constraint so a race condition can't slip past.

Top comments (0)