Rail's out of the box validation feature set is fairly rich. You have the validates
shortcut DSL which maps to all the default validators. The ability to override those default validators through custom validator classes. Finally, you can also write custom validator methods for specific use cases. For the purpose of this article, we'll focus on custom validator methods.
Custom Validator methods
I recently ran across a good use case for custom validator methods. Adding validation to attributes on a serialized object isn't possible by default in Rails without using a custom validator method. Here's our default code logic.
class Company < ApplicationRecord
serialize :settings, CompanySettings
end
class CompanySettings
SETTINGS_HASH = %w(
industry_speciality
company_email
branding_color # We want to validate this!
).freeze
end
Looking at the above code, we can see that Company contains an association called settings
that is based off the class CompanySettings
. This class allows for new columns to be easily added to a settings hash but has the unfortunate downside of preventing the usage of Rail's built-in default validators (since CompanySettings is not a model in this case). Ideally, having every attribute properly validated and tested is the way to go, so finding a solution to this problem was a priority.
A colleague and I came across custom methods within the Rail's validators documentation. This solved our issue via the ability of a custom validation method as well as a matching spec for the new method. An added benefit from this solution, was the freedom in the future to add additional branches and error conditions. See the full solution below.
class Company < ApplicationRecord
serialize :settings, CompanySettings
VALID_COLOR_REGEX = /\A#[0-9a-fA-F]{6}\z/
# Notice how this DSL uses validate instead of validates
#
# The reason for this is two-fold.
#
# 1. validates is the shortcut dsl for built-in validator classes
# 2. In order to use custom validator methods you first need to register
# the method with the validate API
validate :branding_color
def branding_color
if settings.branding_color.present? # This is equivalent to validates allow_blank: true attribute
&& !VALID_COLOR_REGEX.match(settings.branding_color)
errors.add(:settings_branding_color, I18n.t("company_settings.branding_color.error_message"))
end
end
end
# spec/models/company_spec.rb
describe Company do
describe "#branding_color" do
let(:company) { build(:company) }
it "is valid when custom link color is empty" do
expect(company.valid?).to be true
end
it "is valid when custom link color is a correct color hex code" do
company.settings.branding_color = "#abcdef"
expect(company.valid?).to be true
end
it "is invalid with a bad color hex code" do
company.settings.branding_color = "invalid_color"
expect(company.valid?).to be false
end
end
end
The previous code provides a way of properly validating a field that is within a serialized object. We also have the piece of mind that this will function how we expect thanks to a clear spec of the new validation method.
A little on custom validator classes
Generally, custom methods seem to work great for quick or difficult to validate attributes while custom validator classes are beneficial for multiple files that share the same validation method. Custom validator classes also give you the power to override the current implementations of default validators (like email or password) or write your own (MyValidatorClass).
Conclusion
Here are some quick basics for custom validation methods:
- Must register a method with the
validate :method_name
syntax - Enables validation on otherwise difficult to validate attributes (e.g. serialized)
- Allows for multiple error branches and messages
Join the conversation