This One Rails Validation Trick That Makes Your Code More Awesome
Matthew • April 13, 2022
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:
#valid let you pass in a symbol as a validation context.
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
By default, Rails will provide the context
:update when creating or updating an object. (But you can't pass in a context to
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!)
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.
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!