Validation contexts are a little-appreciated but immensely practical feature of Ruby on Rails’ object-relational mapper, ActiveRecord. I cannot count the number of times I have seen hacks around a problem for which a validation context would have been a perfect fit simply because this feature lives a bit under the radar and isn’t in every Rails developer’s toolbox.

What is a validation context, precisely? It is a way to constrain a model validation to a particular usage context for a record. This is similar to what you might achieve with something like state_machine, but far more lightweight.

Let’s say we have an application where we want to dispense gift cards to select users. Administrators can manage an inventory of gift cards and then invite users to claim them by filling out a form at a tokenized link.

A schema for such a feature might look as follows:

class DefineSchema < ActiveRecord::Migration[5.0]
  def change
    create_table :gift_cards do |t|
      t.string :code
      t.string :token
      t.string :name
      t.string :email
      t.datetime :claimed_at

Different validation rules apply in different contexts. In the admin-editing context (which we’ll consider the default context) the record is valid so long as it has a code. In the user-claiming scenario, the gift card is only valid if it has been assigned a name, email and the user has supplied a valid confirmation of its token.

We can tie these contexts to model validations by supplying the :on option to our validates and validate calls. Our gift card model might therefore look as follows:

class GiftCard < ApplicationRecord
  attr_accessor :token_confirmation
  validates :code, presence: true
  # Contextual validations:
  validates :name, presence: true, on: :claim
  validates :email, presence: true, format: /\A\S+@.+\.\S+\z/, on: :claim
  validate :token_match?, on: :claim
  before_create :generate_token
  scope :unclaimed, ->{ where(claimed_at: nil) }
  def claim(attrs={})
    self.attributes = attrs.merge(claimed_at:
    save(context: :claim)
  def token_match?
    unless token_confirmation == token
      errors[:base] << "You are not authorized to claim this gift card"
  # generate a random token for this Gift Card (i.e. for token link authorization)
  def generate_token
    self.token = SecureRandom.hex(10)

The beauty of validation contexts for use cases like we have here is how declarative and readable they are and how foolproof they become once we’re further up in the stack. To drive this point home, let’s have a look at how skinny the controller and UI layers we build around this model to handle the full user flow are.

class GiftCards::ClaimsController < ApplicationController
  before_action :find_gift_card
  def new
    @gift_card.token_confirmation = params[:token_confirmation]
  def create
    if @gift_card.claim(gift_card_params)
      redirect_to gift_card_claim_path(@gift_card)
      render :new
  def show
    # default render
  def gift_card_params
    params.fetch(:gift_card, {}).permit(:name, :email, :token_confirmation)
  def find_gift_card
    @gift_card = GiftCard.find(params[:gift_card_id])

This minimal controller implementation can handle our entire flow of presenting a claim form, processing and validating user input, delivering an email and presenting the user a success page when they are done. The minimal form implementation below is enough to take all the requisite input, as well as keep the token that the user came into the flow with in scope (note: this is using simple_form):

<div class="row">
<div class="col-md-4 col-md-offset-4">
    <%= simple_form_for @gift_card, url: gift_card_claims_path(@gift_card), method: :post do |f| %>
      <%= f.error :base %>
      <%= f.input :name %>
      <%= f.input :email %>
      <%= f.input :token_confirmation, as: :hidden %>
      <%= f.button :submit, "Claim Gift Card" %>
    <% end %>

It is worth noting that as of Rails 4.1, the on option to validate/validates can now take multiple contexts. This is welcome flexibility and in my opinion even further reduces the number of real-world use cases for heavyweight solutions like state_machine.