Boring Rails

Tip: Run different ActiveRecord validations based on context

:fire: Tiny Tips

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?

If you like these tips, you'll love my Twitter account. All killer, no filler.