loading...

Rails Value Objects – One Validator to Rule Them All

databasesponge profile image MetaDave 🇪🇺 Updated on ・3 min read

TL;DR

Validation of all value objects can be achieved with a small custom Validator class.

Problem

When you define a value object in Ruby, and in particular in Rails, it is a natural part of its functionality that it should tell the difference between valid and invalid initialisation inputs.

So a value object would naturally have a #valid? method, and maybe an #errors method as well.

Since Rails gives you the ability to apply validations to attributes, do you also need one custom validator for each value object? Is there a succinct and efficient way of connecting Rails validators and your value objects?

Background

A value object can generally tell us whether it is valid or not. Indeed this might be a major reason for creating a value object, so that it can encapsulate all of the validation logic, and respond to valid? appropriately. @email.valid?, @zip_code.valid?, @isbn.valid?, etc.

For example consider a Country object that is initialised with a two-letter ISO country code such as "GB" or "FR". It can reference a valid codes list to define the list of valid country codes, and various attributes: its name, whether it currently exists or not, perhaps a capital city name.

So when input is accepted we need to check that the input value is valid according to the code list (and maybe we add something to clean up any easily corrected issues like lower case inputs or spaces).

Therefore we might fairly expect that Country.new("XA").valid? will respond with false, and Country.new("x1").errors should respond with ["Country codes must be a two-letter ISO 3166-1 alpha-2 code"]. And you're correct, I do write the best and most precise error messages in the world.

In many cases you will have sanitised the input in front-end logic of course, and for a code from a small list of values or for a simple algorithmic validation, that is always going to help. For more complex cases (is the ISBN in a range that we recognise to have been assigned by ISBN International, for example) we may defer the logic to the server, and rely on a value object for validation anyway.

We probably want to protect our database from invalid values, by using standard Rails validation in the model class:

  validates :country, erm what goes here?

Solution: The Valid Validator

If all of our value objects define the same responses, then the way is open to treat them the same way with a single custom validator.

Here is a class that we can use to validate any object that can enumerate its errors:

class ValidValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    value.errors.each { |error| record.errors.add(attribute, error) }
  end
end

I think that we can all agree that that is an imaginative and accurate name, although I would also accept NoErrorsValidator.

All of the errors read from the value object are added to the record errors, and every value object that implements an errors method can be validated with this same class.

So here's how you use it:

  validates :country, valid: true

... with all of the usual options ...

  validates :country, valid: true, allow_nil: true

allow_nil: true causes the validation to be skipped if the value object responds to nil? with true, so make sure your value objects respond to that – see also Ruby nil value objects in this series.

And here we have it – one ActiveModel validator to rule them all.

  validates :country,             valid: true, allow_nil: true
  validates :construction_method, valid: true, allow_nil: false
  validates :height,              valid: true, allow_nil: false
  validates :occupancy_class,     valid: true, allow_nil: false   

If by "all" we mean "value objects", that is.

Discussion

pic
Editor guide
Collapse
dominh profile image
dominh

This validator should be added to activemodel gem!