This One Rails Validation Trick That Makes Your Code More Awesome

Matthew • April 13, 2022

Smiling emoji face points at a monitor with some Ruby code on it

Rails is filled with small cool features that make life better once you know about them. One that we all know about but might not use that often is validation contexts.

If you are in a situation where an object lives through some kind of workflow or several users are building up an object over time, this might be the tool to help.

A follow-on effect is that validation contexts push you to find good names for states and actions in your application. I've always found that good names clarify concepts and make the code cleaner.

What are validation contexts?

The documentation has a good explanation, but here's a quick recap:

Calls like #save and #valid let you pass in a symbol as a validation context.

post.valid?(:publish)

If we have validations set up like this in a class:

class Post
  validates :title, presence: true, on: [ :publish ]
end

The system will only check for the presence of the title when you pass in the :publish context.

By default, Rails will provide the context :create or :update when creating or updating an object. (But you can't pass in a context to #create or #update!)

You can pass the :on value to most validation DSL commands.

How they can make your code more awesome

Validation contexts drive us to create clearer code with good names and let us show different users only the errors that are meaningful to them.

Many entities evolve through a lifecycle on this website. JobPost is an example. Multiple people will work on a JobPost like a submitter (i.e., customer) and the publisher (me).

This approach lets us, for example, save work in progress. Or it enables the customer to get through the flow with less friction because they don't need to fill in all fields.

The code looks something like this:

class JobPost

  with_options on: [ :submit ] do
    # A minimal set of attributes
  end

  with_options on: [ :publish ]
    # More attributes with stricter requirements
  end

  def submittable?
    valid?(:submit)
  end

  def publishable?
    valid?(:publish)
  end

  def save_as_submitter
    save(context: :submit)
  end

  def save_as_publisher
    save(context: :publish)
  end
end

The small but magical thing, at least for me, is wrapping the call to #valid? in its predicate method. (I'm a little crazy about predicates.) My experience is that this makes for terse, understandable code. It's subtle but impactful, I think.

And once you do that to valid, it seems to make sense to do it for save, too. So, #save_as_publisher reveals itself.

That makes your code more readable, and it also lets you communicate to different errors to the user depending on, err, context. (I've also found that this makes code in controllers more intention revealing!)

Another example

I worked on a project where I had to ingest noisy legacy data. Lots of this data failed the validation rules for the new app, but I couldn't discard it. The solution here was validation contexts.

class Contact
  with_options on: [ :ingest ] do
    validates :state, format: /regex/
    # More validations ...
  end

  with_options on: [ :create, :update ] do
    validates :state, included: %w[ MA NH RI ]
    # More validations ...
  end

  def ingestable?
    valid?(:ingest)
  end

  def save_as_ingester
    save(context: :ingest)
  end
end

This approach let me get most of the legacy data into the new system and let humans resolve problem cases over time. (Or it let them discard it with more confidence and transparency.)

Once I got into the habit of using them, I started finding that I keep finding more places to use them.

In conclusion

Using validation contexts may seem like a slight change in the code, but I've found that it helps me think more clearly about objects. It also prompts me to find names for things, which always seems to clean up code (and feels like a triumph!)

And I think it's neat! Let me know if you've used this feature and how it worked out for you!

Get the latest sent to your inbox once a week!

Receive a weekly update of technical tips, Rails job market analysis, Rails job listings and more!

We'll never share your email address. See our Privacy & Data Policies for more details.


Matthew Bellantoni is a seasoned technology manager experienced growing teams from 0 people to under 100 people, from $0 to $100M in revenue, at companies in SaaS, eCommerce, marketplace, and enterprise software. His time has typically been at fast-growing VC-backed companies, mainly using Ruby on Rails, where he's been senior management usually reporting to the CEO. (He's also written a Ruby on Rails gem with over a million downloads.) You can contact him at matthew@rubyjobboard.com or at @mjbellantoni.