Rails 8 adds conditional allow_nil and allow_blank in model validations

We often need validations that adapt dynamically to the state of a model. At the same time, we want to avoid duplication and keep our code DRY. Rails 8.1 introduced the ability to pass a callable to allow_nil and allow_blank, giving us exactly that: validations that are conditional and easy to maintain.

In this post, we’ll explore this feature through a book publishing workflow. Imagine a Book model with attributes like isbn and status. Draft books are treated differently from published ones, and we want a validation setup that is clear, concise, and maintained in a single place.

Before

In older Rails versions, allow_blank could not be conditional. We duplicated validations to express different behavior. We ended up repeating the same format rule.

class Book < ApplicationRecord
  # attribute :isbn, :string
  enum :status, { draft: 0, submitted: 1, published: 2 }

  validates :isbn,
            format: { with: /\A\d{13}\z/, message: "must be a 13-digit number" },
            unless: :draft?

  validates :isbn,
            format: { with: /\A\d{13}\z/, message: "must be a 13-digit number" },
            allow_blank: true,
            if: :draft?
end

What this means:

When not draft, the isbn must exist and match the pattern. When draft, blank is allowed, but if present it must match. We repeat the format hash in two places. We maintain the pattern twice.

After

After this PR, Rails supports callable allow_nil and allow_blank. We can make the “skip blank” decision depend on model state and keep the format rule in one place.

class Book < ApplicationRecord
  # attribute :isbn, :string
  enum :status, { draft: 0, submitted: 1, published: 2 }

  validates :isbn,
            format: { with: /\A\d{13}\z/, message: "must be a 13-digit number" },
            allow_blank: :draft?
end

What happens:

If draft? returns true, we allow blank. If draft? returns false, we do not allow blank. If isbn is present in either state, the format rule runs. We stay DRY. We keep intent close to the rule.

Alternative with a lambda

We can use a proc or lambda for complex logic.

validates :isbn,
          format: { with: /\A\d{13}\z/, message: "must be a 13-digit number" },
          allow_blank: ->(book) { book.draft? }

Why this matters

  • Cleaner code.
  • Less duplication.
  • Expressive intent.
  • Easier maintenance.
  • One validation to read.
  • One place to change the pattern.

Takeaways

We conditionally allow blank values without duplicating rules. We keep validations aligned with workflow states. We improve readability and long‑term maintenance.

Need help on your Ruby on Rails or React project?

Join Our Newsletter