Better Conditional Validations in Rails
Posted on Monday, May 27th 2013click here for the complete source code
Here at the CoverHound engineering team, we try to keep as close to Rails conventions and standards as possible, so of course we use the baked in ActiveModel::Validations to verify our shopper model has all the information it needs when being created or updated. We ran into a dilemma however, when we wanted to selectively validate specific attributes using the ActiveModel’s validators. There are built-in conditional validations in Rails using :if
and :unless
options:
However, they don’t offer the flexibility we want for our application and are actually a bit inconvenient to use sometimes. The problem is, depending on context, we might want to run a particular validation or we might not. For example, let’s say we have some sort of basic user model and it has two attributes: name
and email
. Let’s also assume that for whatever reason, there are two different forms for creating or editing a user. In one form both the name and email are required and the other only the name. This set of requirements could potentially be done using :if
and :unless
options, but if you have a fair amount of attributes needing this flexibility, it can become ugly really quickly. I thought there’s got to be a better way.
In this write-up, I'll go over a technique to allow more control over what validations get run at what time as well as using validations to determine "completeness" (or validity) of a model at will.
Extending Conditional Validations
So how can we still take advantage of Rails 3 validations while gaining more flexibility for different contexts? We want the solution to be simple and to take advantage of ActiveModel::Validations
. First, we’ll create a new module in our /lib
directory called conditional_validations.rb
. Making a module allows us to easily add its functionality to any model we like:
To get started, we need a way to define what attributes should be validated at any given time; something like a list that can be stored temporarily on model but not actually saved to it. Enter attr_accessor
. We’ll add an attribute called validated_fields
that we’ll use as that list of attributes.
Now our controller code could look something like:
This allows us to explicitly state what fields to validate. Great. Now, we don’t want to stray too far from how the validations are written, so we want something that holds the same form as a regular validation; something that looks like this:
We'll have to create this conditionally_validate
method to execute only those validations that are in the list. We’ll start with something simple:
And this will work, but with a little more effort we can add a secondary :if
clause so you can have even more control:
And lastly, we can add block validation functionality:
You’ll notice it’s a little different for the block validation, whereas your method has to be prepended with validate_
. And with that, we have a fully-functional conditional validation system. We can now define validations on models in the exact same way you already do by just changing validate
to conditionally_validate
.
Verifying Model Completeness
There is actually another advantage of this approach. We can go a step further and add another method that allows us to ask a model with conditional validations if it has particular fields that are valid. We could say:
This is pretty convenient and especially useful for verifying how "complete" a model is. Throughout our auto form, the shopper, vehicle, and driver objects get saved and verified in stages. Using this technique we can quickly figure out at what step the user is at in our process without having to rely on flags or incrementing a "step" number, which can sometimes lead to inaccuracies when changing the form flow or adding new features. Basically, we want to run specific validations on any given model and ensure that it does not actually affect the model -- that is, being able to run .valid?
without it affecting @errors
. Implementing this requires knowing a couple of things:
1.) When validations are run, the class variable @errors
gets modified. Since the module is just a kind of extension onto user.rb
, even running .valid?
on a duplicated mock will change @errors
. We want to know whether certain attributes are valid according to our ActiveModel validators, but we don’t want to modify the @errors
on the model instance we’re validating.
2.) In the same regard, if there were errors on the model initially, then we have to repopulate those errors when we’re done. Let’s try it out:
This gives us great control over exactly what needs to be valid at any given point in the process. In one of our use cases, it helps us tell how "complete" a shopper is in terms of the auto form or the renters form with methods built on top of this, like auto_form_complete?
and vehicles_and_drivers_complete?
using actual validations instead of relying on flags or "steps".
If you'd like to check out the module in its entirety, it is available as a Gist here