There’s one question that comes up soon after starting to use form objects:

Where should I place my validations? In the model or in the form object?

By default validations live in the model.

But if the form object has no validations then it does not validate user input! We decide to move the validations from the model to the form object.

But now it’s possible to create an invalid model in some other part of the code! We decide to copy the validations and add them to both the model and the form object.

But now we are duplicating code! It’s likely that these validations will diverge, that some developer will change some validation in one place and not in the other. We decide to extract the validations into a module (or a concern) and then include it in both the model and the form object.

But this spreads the code over another file and adds another level of indirection — when we open up the model file it’s not immediately obvious what its validations are.

There should be a better solution.


We can place most of the validations (more on this later) on the models. On the form object we can delegate down validations to each “child model” and promote up any errors found in the models.

Let’s check an example of a form object:

class Registration
  include ActiveModel::Model

  attr_accessor :email, :password, :country, :city

  def save
    ActiveRecord::Base.transaction do
      user.save!
      location.save!
    end
  end

  private

  def user
    @user ||= User.new(email: email, password: password)
  end

  def location
    @location ||= user.build_location(country: country, city: city)
  end
end

We can reuse the validations of user and location like this:

class Registration
  # ...

  validate :validate_children

  def save
    return false if invalid?

    # ...
  end

  private

  def validate_children
    if user.invalid?
      promote_errors(user.errors)
    end

    if location.invalid?
      promote_errors(location.errors)
    end
  end

  def promote_errors(child_errors)
    child_errors.each do |attribute, message|
      errors.add(attribute, message)
    end
  end
end

Calling invalid? in the save method runs all the validations, including the validate_children method. Since there’s no easy way to move all the errors from one object to another, we iterate over all the errors and add them one by one to the form object.

We can extract some of this logic into a base FormObject (or a concern) if we start using this pattern a lot.


Still, there are some validations that should live in form objects.

I like to divide validations into two groups: data integrity validations and business logic validations.

Data integrity validations are concerned with the fidelity and quality of the data saved to the database. All locations must have non-empty country and city attributes so this should be validated in the model:

class Location < ApplicationRecord
  validates :country, presence: true
  validates :city, presence: true
end

These are database rules. Ideally these rules are mirrored on the database via its schema and its constraints:

ALTER TABLE locations
ALTER COLUMN country SET NOT NULL,
ALTER COLUMN city SET NOT NULL;

On the other hand, business logic validations are concerned with the appropriateness and completeness of the data going through a certain workflow. The email registration workflow requires users to enter an email and accept the terms of service so this should be validated in the form object:

class EmailRegistration
  include ActiveModel::Model

  validates :email, presence: true
  validates :terms_of_service, acceptance: true
end

These are contextual rules. These rules only apply to this particular use case. Think of how much harder it would be to create a phone registration workflow if the email validation lived in the User model.

Contextual rules enforced on the model level are global rules.

Whenever you find yourself needing to skip a validation in certain situations or needing to configure when a validation should run with the :on option, try to see if there’s a way to extract that validation and that logic into a form object.


We now want to ensure that emails entered in the email registration workflow are unique. Since the email presence validation lives in the EmailRegistration form object we’d be inclined to add the new validation there:

class EmailRegistration
  include ActiveModel::Model

-  validates :email, presence: true
+  validates :email, presence: true, uniqueness: true
end

Yet, if the user is able to change his email later on, he can pick an email that is already taken. We can’t let this happen. If a user has an email then it must be unique. This is a data integrity validation and it should live in the model:

class User
+  validates :email, uniqueness: true, allow_blank: true
end

Learning to distinguish between these two types of validations is essential to the design and structure of our applications.

Conclusions

  • Don’t mix data integrity validations with business logic validations.

  • Place data integrity validations inside models.

  • Place business logic validations inside form objects.

  • Promote model errors to form object errors.

Now go on and create your own form objects. Add contextual validations. Simplify your code!