Rails 8.1 introduced except_on option for validations and callbacks

Rails 8.1.0 introduces the except_on option for validations and validation callbacks, making it easier to skip them in specific contexts.

In Rails, we can conditionally run validations. For example, we can run validations in certain contexts by using the on option.

Validations

Before

There was no direct way to exclude validations based on context (e.g., skipping validations during an update). We had to rely on custom conditional logic.

  • The on: option had limited scope, allowing validations only during create or update.
class Client < ApplicationRecord
  validates :name, presence: true, on: [:create]
end
Client.create!(name: "")
`<main>': Validation failed: Name can't be blank (ActiveRecord::RecordInvalid)
client.update!(name: "")

Client Update (2.2ms)  UPDATE "clients" SET "name" = ?, "updated_at" = ? WHERE "clients"."id" = ?  [["name", ""], ["updated_at", "2025-12-17 14:49:27.360920"], ["id", 1]]
=> true
  • Using conditionals increased code complexity.
class Client < ApplicationRecord
  validates :name, presence: true, if: :new_record?
end
Client.create!(name: "")
`<main>': Validation failed: Name can't be blank (ActiveRecord::RecordInvalid)
client = Client.create!(name: "Google")
  TRANSACTION (0.2ms)  BEGIN immediate TRANSACTION
  Client Create (10.2ms)  INSERT INTO "clients" ("name", "created_at", "updated_at") VALUES ('Google', '2025-12-17 15:34:32.155417', '2025-12-17 15:34:32.155417') RETURNING "id"
  TRANSACTION (0.3ms)  COMMIT TRANSACTION
=> #<Client:0x00000001118b3208 id: 5, name: "Google", created_at: "2025-12-17 15:34:32.155417000 +0000", updated_at: "2025-12-17 15:34:32.155417000 +0000">
> client.update!(name: "")
  TRANSACTION (0.3ms)  BEGIN immediate TRANSACTION
  Client Update (3.2ms)  UPDATE "clients" SET "name" = '', "updated_at" = '2025-12-17 15:34:50.928492' WHERE "clients"."id" = 5
  TRANSACTION (0.4ms)  COMMIT TRANSACTION
=> true
  • Manually skipping validations by using client.save(validate: false) was risky and generally not recommended.

After

Rails added except_on option for validations to skip validations.

class Client < ApplicationRecord
  validates :name, presence: true, except_on: :update
end
c = Client.create!(name: "")
`<main>': Validation failed: Name can't be blank  (ActiveRecord::RecordInvalid)
c.update!(name: "")
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION
  Client Update (0.8ms)  UPDATE "clients" SET "name" = '', "updated_at" = '2025-12-17 15:07:35.495104' WHERE "clients"."id" = 3
  TRANSACTION (0.2ms)  COMMIT TRANSACTION
=> true

Validation Callbacks

Before

The same challenges applied to validation callbacks. We had to rely on conditional logic to skip callbacks in certain contexts.

class Client < ApplicationRecord
  before_validation :set_name, if: :new_record?

  validates :name, presence: true

  def set_name
    self.name = "Hello World" if name.blank?
  end
end
Client.create!(name: "")
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION
  Client Create (7.8ms)  INSERT INTO "clients" ("name", "created_at", "updated_at") VALUES ('Hello World', '2025-12-17 15:24:31.331285', '2025-12-17 15:24:31.331285') RETURNING "id"
  TRANSACTION (0.3ms)  COMMIT TRANSACTION
=> #<Client:0x000000010f17f5d8 id: 4, name: "Hello World", created_at: "2025-12-17 15:24:31.331285000 +0000", updated_at: "2025-12-17 15:24:31.331285000 +0000">
client.update!(name: "")
`<main>': Validation failed: Name can't be blank (ActiveRecord::RecordInvalid)

After

Rails introduced the except_on option for validation callbacks to allow skipping them in specified contexts.

class Client < ApplicationRecord
  before_validation :set_name, except_on: :update

  validates :name, presence: true

  def set_name
    self.name = "Hello World" if name.blank?
  end
end
client = Client.create!(name: "")
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION
  Client Create (7.8ms)  INSERT INTO "clients" ("name", "created_at", "updated_at") VALUES ('Hello World', '2025-12-17 15:24:31.331285', '2025-12-17 15:24:31.331285') RETURNING "id"
  TRANSACTION (0.3ms)  COMMIT TRANSACTION
=> #<Client:0x000000010f17f5d8 id: 4, name: "Hello World", created_at: "2025-12-17 15:24:31.331285000 +0000", updated_at: "2025-12-17 15:24:31.331285000 +0000">
client.update!(name: "")
`<main>': Validation failed: Name can't be blank (ActiveRecord::RecordInvalid)

Conclusion

The addition of except_on: to both validations and callbacks streamlines conditional exclusions, making it easier to manage model behavior for different contexts in Rails applications.

References

Need help on your Ruby on Rails or React project?

Join Our Newsletter