ActiveModel validations in Rails are awesome, but what happens when your UI doesn’t exactly match your data model? Or when you want multiple levels of conditional validations?

A useful web application requires a data model that accurately represents business logic and a user interface that is intuitive and attractive, but sometimes balancing both can be difficult. It’s important that while your client and your users SEE the UI and don’t see the backend, you, as the developer, remember that they are both of equal importance for a successful business. To achieve this, let’s see how we can provide an experience our users deserve while preserving the integrity of the application.

Stepped Forms

For example, lets say we’re building an application for people to find housing, similiar to Trulia or Redfin. At a minimum, we’ll need a ‘Property’ model with some basic data such as name, street_address, unit, city, state, and zip. Then we’ll need more information such as bedrooms, bathrooms, parking spaces, yard, jacuzzi, etc… We don’t want to put this on one giant form because giant forms are a turn-off. So we’ll have multiple forms to collect all of this data. Since we want the experience to be easy for the user, we want to save and validate the data as the user progresses through the forms.

For the sake of this post, I’m assuming that putting all required fields on the first form does not make sense for this application.

One approach to handling implementing this is to rely solely on client validations, but I highly recommend NOT doing this. Client validations are for UX, not for actual validation. ALWAYS validate on the server.

Another approach is to break up the ‘Property’ model into several models, such as ‘Address’, ‘Basic’, and ‘Details’ models, each with its own validations. This can work for validations, but may not make sense down the road. If you ALWAYS need the data from all of the models then anytime you fetch a property, you need to fetch the data from 3 tables, if you want to update an attribute, you need to know to which model it belongs, and when you add data (this always happens) you can end up with a growing number of classes. Put simply, if you always need all of the data, then it doesn’t make sense to split it up into separate models. DO NOT change your data model just to support the UI.

There are many ways to solve this problem, but I’m going to propose one that leverages a little known feature of ActiveRecord called ‘context’. That post does a great job of introducing the concept so I won’t dwell on it, let’s dive into using it for our housing application. Let’s break up the data into the 3 sections listed above and using ‘context’ we can write the validations like so:

class Property < ActiveRecord::Base

  ...

  validates :name, presence: true

  with_options on: :address do
    validates :street_address,
              :unit,
              :city,
              :state,
              :zip,
              presence: true
  end

  with_options on: :basic do
    validates :bedrooms,
              :bathrooms,
              :square_feet,
              :basement,
              presence: true
  end

  with_options on: :details do
    validates :yard,
              :porch,
              :pet_friendly,
              presence: true
  end

  ...

end

This allows us to do a couple cool things. First, we can specify the context when saving the model to isolate which validations will run with

property.save(context: :address)

which will only run the validations inside the

with_options on: :address

block. Second, we can also use the context when determining validity:

property.valid?(:address)

To implement this in our controller we can either implement a custom action:

def update_address
  @property = Property.find( ... )
  @property.attributes = property_params
  @property.save(context: :address)

  ...

end

or using a query parameter:

def update
  @property = Property.find( ... )
  @property.attributes = property_params
  @property.save(context: params[:context].to_sym)

  ...

end

Now we can validate specific groups of attributes and communicate the status of each group to the client using the results

valid? :context

for each context. This is great for displaying the overall progress through the stepped form to the user.

Continuing on, let’s say we are not going to post the property until all contexts are complete, how do make sure all of these run? Easy, just add a new context to each group!

Update each group to include the ‘:complete’ context:

with_options on: :address

becomes

with_options on: [:address, :complete]

Then we can ensure all of them run when saving with

property.save(context: :complete)

Awesome!!

This is incredibly powerful and is extremely helpful when the state or your model depends on the presence of certain data.

Conditional Validations

Let’s change gears a little bit and talk about conditional validations. Rails makes this pretty easy to do in simple cases using a similar approach as above. For example, let’s say I want to validate the type of pets allowed in the property, but only if they specify that pets are allowed:

validates :pet_type, presence: true, if: :pet_friendly?

or, just like we did above:

with_options if: :pet_friendly? do
  validates :pet_type, presence: true
end

Pretty simple, but what if the conditions were nested? In a completely contrived example, let’s say the user can choose ‘dog’ or ‘cat’ for pet type and then we require them to choose the type of dog or cat, depending on what they chose. Your first approach might be to do something like:

with_options if: :pet_friendly? do
  validates :pet_type, presence: true, inclusion: { in: %w(dog cat) }

  validates :dog_type, presence: true, if: "pet_type === 'dog'"
  validates :cat_type, presence: true, if: "pet_type === 'cat'"
end

but this doesn’t work! The options passed to the validations are not nested as you would expect, but are actually merged. This means the options passed to the ‘dog_type’ validation is equivalent to:

{ if: :pet_friendly? }.merge({ if: "pet_type === 'dog'" })

or

{ if: "pet_type === 'dog'" }

yikes!

So to handle this we can either add all conditions to each validation:

validates :dog_type, presence: true, if: [:pet_friendly, "pet_type === 'dog'"]

or use different options, which would probably lead to problems down the road when someone decides to refactor your code:

with_options if: :pet_friendly? do
  validates :pet_type, presence: true, inclusion: { in: %w(dog cat) }

  validates :dog_type, presence: true, unless: "pet_type === 'cat'"
  validates :cat_type, presence: true, unless: "pet_type === 'dog'"
end

Unfortunately, I don’t have a magic bullet for this issue, but I do think using context can help simplify a tiny bit. I only want to validate the pet information within the ‘:details’ context:

with_options on: [:details, :complete] do
  with_options if: :pet_friendly? do
    validates :pet_type, presence: true, inclusion: { in: %w(dog cat) }
  end
end

which gives me 2 conditions or a validation in a pretty clean and understandable way. This should handle 90% of the cases out there, if you need one more, then including a list with all the conditions should suffice. If you need more, than you may want to find a different solution or evaluate whether you need that level of complexity.

I hope these tricks are as helpful to you as they are to me, any thoughts, suggestions, criticisms, or comments? Drop us a line at launchpadlab.com

The LaunchPad Lab Team

Our team is a collective of curious minds, problem solvers, and tech enthusiasts. Beyond our dedication to building innovative digital products that drive business results, we're passionate about sharing our knowledge and insights through engaging content — offering articles on the latest tech trends, practical advice on product development, and strategies to harness technology for competitive advantage.

Reach Out

Ready to Build Something Great?

Partner with us to develop technology to grow your business.

Get our latest articles delivered to your inbox