Encapsulate each validation error as an Error object


Rails modified the way errors are represented when a model save, create or update action fails.

Before

Let’s say we have a User model, with columns like first_name, last_name, contact_number, email and all of them are mandatory. If we try to create a User object without passing first_name and contact_number as a string, the #errors function will show the errors as below

class User < ApplicationRecord
  validates :contact_number,
            presence: true,
            numericality: true,
            length: { :minimum => 10, :maximum => 15 }
end

user = User.create(email: "sam@example.com", last_name: "Example", contact_number: "abcdefghijk")

user.errors
=> #<ActiveModel::Errors:0x00007fe42c1650b8
  @base=
  #<User:0x00007fe42c1676d8
  ....
  ....
  @details={:first_name=>[{:error=>:blank}], :contact_number=>[{:error=>:not_a_number}]}
  @messages=
  {:first_name=>["First Name is required."],
   :contact_number=>["Contact number is not a number."]
  }>

The error message for a particular field can be accessed using the [] method as below

user.errors[:first_name]
=> ["First Name is required."]

We could also use #messages or #full_messages to see the list of all errors.

user.errors.messages
{
  :first_name=>["First Name is required."],
  :contact_number=>["Contact number is not a number."]
}

user.errors.full_messages
[
  "First name is required.",
  "Contact number is not a number."
]

Accessing errors in the above manner is not object oriented. If a particular field has multiple errors associated, it has to be accessed using array indexes.

user = User.create(email: "sam@example.com", last_name: "Example")

user.errors[:contact_number]
=> ["Contact Name is required.", "Contact Name is not a number."]

user.errors[:contact_number][0]
=> "Contact Name is required."

user.errors[:contact_number][1]
=> "Contact Name is not a number."

After

With recent changes in ActiveModel#errors class the above errors will appear as object of Error class instead of hash.

user = User.create(email: "sam@example.com", last_name: "Example", contact_number: "abcdefghijk")

user.errors
=> #<ActiveModel::Errors:0x00007ff5ba2be5a0 @base=#<User id: nil, first_name: nil, last_name: "Example", email: "sam@example.com", contact_number: "abcdefghijk", created_at: nil, updated_at: nil>,
@errors=[<#ActiveModel::Error attribute=first_name, type=blank, options={}>, <#ActiveModel::Error attribute=contact_number, type=not_a_number, options={:value=>"abcdefghijk"}>]>

We can now use where clause to fetch the error(s) related to a particular attribute.

user.errors.where(:contact_number)
=> [<#ActiveModel::Error attribute=contact_number, type=not_a_number, options={:value=>"abcdefghijk}>]

We have methods like add, added?, delete, match? which have similar signature to where => (attribute, type, options).

user.errors.add(:contact_number, :too_short, count: 10)
=> <#ActiveModel::Error attribute=contact_number, type=too_short, options={:count=>10}>

user.errors.where(:contact_number)
=> [<#ActiveModel::Error attribute=contact_number, type=not_a_number, options={:value=>nil}>, <#ActiveModel::Error 
attribute=contact_number, type=too_short, options={:count=>10}>]

user.errors.added?(:contact_number, :too_short, count: 10)
=> true

user.errors.added?(:contact_number, :too_short)
=> false

user.errors.delete(:contact_number, :too_short, count: 10)

user.errors.where(:contact_number)
=> [<#ActiveModel::Error attribute=contact_number, type=not_a_number, options={:value=>nil}>]

user.errors.match?(:contact_number, :not_a_number)
=> true

user.errors.match?(:contact_number, :too_long)
=> false

As seen above, we can verify presence of a specific error in a given attribute by using added? method.

With the new changes added, message or full_message method can be accessed using where clause.

user.errors.where(:first_name, :blank).last.message
=> "can't be blank"

user.errors.where(:contact_number, :not_a_number).last.full_message
=> "Contact number is not a number"

NOTE

Below methods are deprecated which will start showing deprecation warnings if used -

  • []
  • each{|attr, msgs|}
  • generate_message
  • has_key?
  • key?
  • keys
  • values
user.errors.keys
DEPRECATION WARNING: ActiveModel::Errors#keys is deprecated and will be removed in Rails 6.2.

To achieve the same use:

  errors.attribute_names

Below methods have not been changed -

  • as_json, blank?, clear, count, empty?
  • add
  • added?
  • full_messages
  • full_messages_for
  • include?, size, to_hash, to_xml, to_a
  • messages
  • details