Conditional Validations with Rails

Adding validations to your ActiveRecord models always starts out pretty simple. But as your app grows, your business rules compound in complexity and your data can become untidy. Oftentimes we require that certain models are validated differently under different contexts. Here are some examples that I’ll talk about:

  1. Validating a field conditionally upon the value of a different field.
    eg: Listings require a photo only if they are published.
  2. Validating a field conditionally upon who is saving the record.
    eg: Admins can make a shop name anything, while normal users must conform to a specific format.
  3. Validating a field only on a specific form.
    eg: Users signing up through your normal sign-up form must accept a terms of service.
  4. Adding a new field to an existing record that can’t be backfilled.
    eg: You want new users to give you their phone number, but you don’t have phone numbers for existing users.

What does Rails give us?

Rails gives us a few tools as part of the validation API, outlined here in their RailsGuides. Here’s how we might use the ‘if’ option with the validates method to accomplish use case #1 above:

We can abuse this further by adding some custom methods on our model. See how we might accomplish our special admin validations (#2 above):

Rails also gives us the ‘on’ option that allows us to specify validations that should happen only on create or update. With it, we can create a functional solution to #3 above:

https://gist.github.com/joekur/d9d552a795e05fcecac2

This isn’t a perfect solution though, because now if we want to create users elsewhere (for example, from an admin screen), we would still need to pass this attribute. We could utilize our “edit_as_admin!” method as in the previous section again, but since this validation really only applies to one specific workflow in our app — the new user signup, I think it ideally calls for a different approach. Enter the “form object”.

Form objects

Form objects are table-less models — they quack a lot like ActiveRecord, but they don’t actually save to a database. They represent the state and validation of forms themselves. Given the example above, I might consider refactoring my signup action to use a UserSignupForm. With Rails 4, we now have a single mixin — ActiveModel::Model — that makes this very straightforward. Here’s how you might implement such an object, solving our TOS validation:

Besides being a good place to have use-case specific validations, form objects gives us a lot of other benefits. We can easily handle non-persisted attributes, save multiple objects with more flexibility than “accepts_nested_attributes_for”, and even represent errors external to data validations (from talking to 3rd parties, for example) in the same way we represent our validation errors.

Note: Rails does have an ‘acceptance’ validation especially for this use-case. Use it if it works for you, but the idea above still stands.

Making use of modules

One issue that may arise from breaking up all your separate use-cases into form objects is introducing duplication in your validations. If you can imagine having both a UserSignupForm and a UserEditForm (and maybe even an AdminUserEditForm), duplicating validations across those forms quickly becomes a pain. Now of course if it makes sense, you can keep some shared validations in the model itself. If you can’t, you can still clean things up by grouping validations into sensible modules.

Here’s how you might extract validations into a reusable mixin:

As always, use the right tool for the job

Obviously none of these techniques are a silver bullet, and you need to decide what makes the most sense for a given case. Hopefully if you’ve learned a new technique from this post, you’ll have an alternative to stuffing every piece of validation into your model.

With that in mind, let’s think about how we would solve situation #4 — adding a new attribute without a backfill. Well we could put it in the model with a condition:

But if you want to require the phone number only on a signup form, or possibly on the signup and account edit forms, then it might make sense to put the validation on a form object (and maybe use a mixin).

Favor a technique not covered in the post? I’d love to hear about in the comments below. Happy validating!

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response