Tip: Run different ActiveRecord validations based on context
Sometimes want to skip certain validations on your database models. Maybe you have a multi-step wizard or want admins to have more freedom in changing data.
You might be tempted to have certain forms skip validations, but there is a better way.
Rails allows you to pass in a context
when saving or validating a record. You can combine context
with the on:
option to run only certain ActiveRecord validations.
Usage
Let’s say you want want to build a multi-step workflow for a job board. You might allow someone to create a job listing and start filling in the data, but not fully validate everything until it is time to publish the listing.
class Listing < ApplicationRecord
belongs_to :company
belongs_to :user
has_rich_text :requirements
validates :title, presence: true, length: { maximum: 50 }
validates :salary_range, presence: true, on: :publish
validates :application_instructions, presence: true, on: :publish
def publish!
self.published_at = Time.current
save(context: :publish)
end
end
In this case, we will always require a title
(with 50 characters max) whenever we create or edit this record. But we will only validate salary_range
and application_instructions
when we pass :publish
as the validation context.
You could implement this workflow with controller actions like:
class ListingsController < ApplicationController
def create
@listing = Listing.new(listing_params)
if @listing.save
redirect_to @listing, notice: "Listing created"
else
render :new, status: :unprocessable_entity
end
end
def publish
@listing = Listing.find(params[:id])
if @listing.publish!
redirect_to @listing, notice: "Listing published"
else
render :edit, status: :unprocessable_entity
end
end
end
You can also add validations that are different based on which user is making the change. Maybe you want to allow admins to give special, short usernames to your friends.
Here we can set one rule that requires six character usernames in the :create
context (which Rails will include by default when creating a record). Then we add a rule in the :admin
context that only requires three characters.
class Account < ApplicationRecord
validates :username, length: { minimum: 6 }, on: :create
validates :username, length: { minimum: 3 }, on: :admin
end
Account.new(username: "swanson").valid? # => true
Account.new(username: "swanson").valid?(:admin) # => true
Account.new(username: "mds").valid? # => false
Account.new(username: "mds").valid?(:admin) # => true
Account.new(username: "a").valid? # => false
Account.new(username: "a").valid?(:admin) # => false
One downside with using Rails validation contexts is that you may not be able to use database level validations. Being able to persist partially-valid records or have conditional rules is a powerful feature, but it’s not without costs.
Think carefully about moving validations from the database constraint level to your application.
Additional Resources
Rails Guides: Validations :on option
Rails API Docs: ActiveRecord::Validations#valid?