Rails validations and multi-page forms

Ministry of Justice Digital & Technology
Just Tech
Published in
5 min readOct 17, 2018

by Stephen Richards (Software Development Profession)

Standard Rails validations

I was recently running a small training session where we were talking about the Rails validation helpers. These are one-liners in a Rails model that enable you to carry out common validations, for example:

This statement will cause the validations to be run either when you call the #valid? method on a person record, or when you create, save or update a record to the database. If either name, date_of_birth or ni_number is nil or an empty string, then two things happen: an errors object on the person record will be populated, and #valid? will return false. In the case of create, save or update the record will not be saved to the database, and false will be returned. If you used save!, create! or update! an ActiveRecord::RecordInvalid exception will be raised.

The Active Record Validations Rails Guide provides an excellent reference source for the various validations that can be done in this way.

Options for conditional validations

So far so good. But how, asked one of the attendees, do you use Rails validations when you have a typical Government Digital Services (GDS) form where one question is asked per page in a series of pages?

For those of you unfamiliar with GDS form guidelines, they prefer one question per page to build up a body of information before finally creating a record. Take a look at the Register to Vote service for a good example of this pattern.

For Rails validations, this presents a bit of a problem. The validates helper methods are blunt instruments — in their simplest form they always carry out the validations. I’ve seen various solutions to this dilemma, for example:

Using the session to hold partial data: As you progress through the pages, the data is held in the session, and only validated and written to the database once all the information has been entered. I think this has several downsides:

  • The record has to be serialised and de-serialised each time
  • Validations don’t take placed until the very end of the process, so you have to handle moving to the correct page in order to display the error messages
  • It’s tricky to implement “save and return”, in other words fill in the first few pages, save your progress so far and return at a later date

Using the standard validation methods, but with conditions: for example

This requires the controller to set a create_stage variable on the model to record how far along the process it is. This works well enough for a very simple model and form, but can quickly become unmanageable.

Use a custom validation class: This is my favourite method when validations become complex and conditional. We abstract all the validation code out of the model into a specialised class, which helps keep the model from getting too bloated.

Custom Validation Classes

Let’s work through how we’d go about creating a custom validation class.

First of all, we’ll add another column to the database table to record what stage of validation we’re at:

rails generate migration AddCreateStageToPerson create_stage:integer

And then run rake db:migrate to update the database.

Next, we’ll remove the validates line from the model, and replace it with one that tells it to use a validator class.

I put the validators in the /app/validators folder. Any files in subdirectories of /app are automatically loaded by Rails, so we don’t have to do anything special to make sure they are loaded. The validator class must derive from ActiveModel::Validator, and have a method called validate taking the record to be validated as a param. This is where our validation code goes.

So our validator class will look something like this:

The Code Explained

Every time valid? is called on a Person record, an instance of PersonValidator is instantiated, and the validate method is called with the person record passed in as a parameter. In the validate method above, we first save the person record as an instance variable to save passing it around all the methods, and then use a bit of Ruby metaprogramming magic:

__send__(“validate_stage_#{@record.create_stage}”)

This __send__ simply means call the method on the current object. In this case, if the create_stage attribute on the record is 2, this will call the method validate_stage_2.

The real substance of the validations is carried out in the validate_present method. We’re using Ruby metaprogramming techniques again in the line

unless @record.__send__(attr).present?

Here, were calling a method on the @record instance variable using attr as the name of the method, so if attr were :name, this would be the equivalent of

unless @record.name.present?

If it is invalid, we simply add an error message into the errors hash against the attribute.

If at the end of the PersonValidator#validate method there are no messages in the error hash, Rails will consider the record to be valid.

Conclusion

All of this is obviously a lot more work than simply adding a validates line to the model, but it has several advantages for complex validations:

  • It abstracts all of the validations out of the model. We should all strive for Thin Controllers and Fat Models. But don’t just move everything into a model class — in a complex app you’ll end up with huge classes. This is one technique to avoid having model classes with hundreds of lines of code.
  • It makes the validations very easy to test. Just pass in a Person object with the variables set accordingly and assert that the right error messages are set. Here’s a snippet:

None of these records in the above tests have been persisted to the database, so tests run snappily (slow running tests are a real bugbear of large projects, so anything you can do to keep the tests running quickly is to be welcomed).

  • Validation rules can be easily changed and tested in isolation — the ability to make changes easily and quickly is always a major plus in my book.

Obviously, the example we used here is a trivial validation problem, but validations can get extremely complicated, and extracting the validations and the validation tests out of the model can help in keeping the code clean and easy to understand.

~~~~~~~~~~~~~~~~~~~~

If you enjoyed this article, please feel free to hit the👏 clap button and leave a response below. You also can follow us on Twitter, read our other blog or check us out on LinkedIn.

If you’d like to come and work with us, please check current vacancies on our job board (filter on Organisation::Ministry of Justice, Job Role::Digital)!

~~~~~~~~~~~~~~~~~~~~

--

--

Ministry of Justice Digital & Technology
Just Tech

We design, build and support user-centred digital and technology services for the justice system.