Performing custom validations in Rails — an example
Rails provides a variety of helpers out of the box for quickly performing commonly used validations — presence, numericality, uniqueness, etc. If the model has validations that go beyond the standard helpers, we must implement a custom validation strategy. I will show three ways of going about validations and discuss pros and cons.
For example, we have a Shipments table where each record represents a package with attributes width
, height
, depth
, and weight
.
Each shipment must adhere to the following rules:
- The volume of the shipment must be between 20 and 4000 cubic centimeters (ie. volume validation)
- The density of the shipment cannot exceed 200 grams per cubic centimeter (ie. density validation)
- No side length can be less than 10% of the largest side (ie. proportion validation)
For each of these cases, we will use a different validation implementation. In order to make the validations more readable, we implemented the #volume and #density in the Shipment model (check out the source code).
First, let’s validate the shipment volume by creating a custom method in the Shipment class. We can use #validate
to call a custom method during the validation. Then, in the custom method, add new errors to the #errors
object (which deserved its own short post)
class Shipment < ActiveRecord::Base
…
validate :volume_limits
… private def volume_limits
if volume > 4000
errors.add(:volume, “cannot be above 400 cubic inches”)
elsif volume < 20
errors.add(:volume, “cannot be below 20 cubic inches”)
end
end
end
Performing validations within the model works fine, but it also adds more logic to the model. I prefer to extract that logic to its own helper class when possible. That’s because it nicely encapsulates each validation’s logic to its own object, making it easier to debug and/or extend in the future.
Let’s validate the density by creating a helper validator class. The #validates_with
method points the validation at a helper class:
class Shipment < ActiveRecord::Base
…
validates_with DensityValidator
…
end
Then, in the /models/concerns directory, create “density_validator.rb”. The DensityValidator
inherits from ActiveModel::Validator
, whose convention is there to be a method called #validate
. The method has access to the entire record and implements the validation logic, assigning errors if needed:
class DensityValidator < ActiveModel::Validator
def validate(record)
if record.density > 20
record.errors.add(:density, “is too high to safely ship”)
end
end
end
The last validation is to ensure that packages are not oddly shaped. A package is said to be oddly shaped if any side’s length is shorter than 10% of the longest side.
For both of the examples above, we were validating properties of the shipment as a whole — there is a single volume and a single density for any package. In case of the package’s shape, each side must be validated separately.
Using #validates, we list all of the attributes to undergo validation, followed by package_proportion: true
:
class Shipment < ActiveRecord::Base
...
validates :height, :width, :depth, package_proportion: true
...
end
The package_proportion flag means that we expect there to be a validation helper class named PackageProportionValidator. In the /models/concerns directory, create “package_proportion_validator.rb”.
class PackageProportionValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value < [record.width, record.height, record.depth].max * 0.1
record.errors.add(attribute, "cannot be so short as to make
the package oddly sized :(")
end
end
end
The attribute validation helper class expects a #validates_each
method to facilitate the validation. It provides access to the individual attribute currently being validated and the entire record as separate variables, which is helpful.
Let’s trip all of these validations with an oddly shaped, too big and too heavy package.
These are three different ways of implementing validations in Rails. Which one is best will depend on the application goals and the validation use case. I usually try to encapsulate the logic to a separate class to keep the validation logic away from the model. When the validation happens at the attribute level (proportion example), I use the validates_each strategy to have access to the attribute separately. When the validation happens at the object level, I use a custom validator for the entire object (density example).
Thank you for reading. I’m new at writing about code, so I’d appreciate any feedback that helps be a better writer and communicator. Also open to future topics!
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMI family. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!