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]
endClient.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?
endClient.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
endc = 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
=> trueValidation 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
endClient.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
endclient = 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
