ruk·si

🛤️ Ruby on Rails
Validation

Updated at 2015-08-13 19:16

Related to Ruby Guide and Ruby on Rails Guide.

Use model validations.

# these methods trigger model validation
# bang (!) variants raise exception if it fails
create
create!
save
save!
update
update!

# valid? tells if a record is valid
class Person < ActiveRecord::Base
  validates :name, presence: true
end
p = Person.new
p.errors.messages # => {}
p.valid?          # => false
p.errors.messages # => {name:["can't be blank"]}

Use the concise validation variant.

# bad
validates_presence_of :email
validates_length_of :email, maximum: 100

# good
validates :email, presence: true, length: { maximum: 100 }

Use validation helpers:

class Person < ActiveRecord::Base
  validates :name, :login, :email, presence: true

  # checkbox must be checked
  validates :terms_of_service, acceptance: true
  validates :terms_of_service, acceptance: { accept: 'yes' } # what is accepted?

  # related models need to be valid on save too
  has_many :books
  validates_associated :books

  # must also have email_confirmation field speficied with the same value
  validates :email, confirmation: true
  validates :email_confirmation, presence: true

  # validate by regex
  validates :legacy_code, format: {
    with: /\A[a-zA-Z]+\z/,
    message: "only allows letters"
  }

  # validate by whitelist
  validates :size, inclusion: {
    in: %w(small medium large),
    message: "%{value} is not a valid size"
  }

  # validate length
  validates :name, length: { minimum: 2 }
  validates :bio, length: {
    maximum: 500,
    too_long: "%{count} characters is the maximum allowed"
  }
  validates :password, length: { in: 6..20 }
  validates :registration_number, length: { is: 6 }

  # change how the length it calculated with tokenizer
  validates :content, length: {
    minimum: 300,
    maximum: 400,
    tokenizer: lambda { |str| str.split(/\s+/) },
    too_short: "must have at least %{count} words",
    too_long: "must have at most %{count} words"
  }

  # allow only numeric values
  validates :points, numericality: true
  validates :games_played, numericality: { only_integer: true }
  validates :games_bought, numericality: { greater_than: 10 }
  validates :games_viewed, numericality: { even: true }
end

Note that validates_presence_of doesn't validate booleans properly.

# bad
validates_presence_of :protected

# good
validates_inclusion_of :protected, in: [true, false]

Beware of the behavior of the update_attribute method. It doesn't run the model validations unlike update_attributes and could easily corrupt the model state.

Use custom validators if used more than once. Really help to keep code easy to maintain. Keep custom validators under app/validators.

# bad
class Person
  validates :email, format: {
    with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
  }
end

# good
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      record.errors[attribute] << (options[:message] || 'is not a valid email')
    end
  end
end

class Person
  validates :email, email: true
end

Consider creating a validator gem. Consider extracting custom validators to a shared gem if you're maintaining several related apps and the validators are generic enough.

Sources