Dry-validation Basics
All of this needs to be checked to prevent unexpected errors from happening. Although ActiveModel::Validations is great for web apps and simple models, it isn't very flexible for other types of validation and complex dependencies.
Luckily, we have an alternative called dry-validation. It is one of the tools provided by the dry-
In this
Basics
Installation is pretty simple, you just need to add the dry-validation gem to your Gemfile and require it. We are going to validate this simple data structure.
{
name: 'John',
surname: 'Doe'
}
The first thing we need to do is define the validation schema.
require 'dry-validation'
validator = Dry::Validation.Schema do
# validation rules
end
As you can see it takes a block as a parameter. We will add validation rules inside of it. You can think of schemas as classes that contain validation rules. A schema with no rules will always pass for any input. You can check it yourself by using UserValidator like a proc object (it responds to call).
validator.call('some input').success? #=> true
validator.call({}).success? #=> true
validator.call([]).success? #=> true
If you are familiar with dry-monads (yet another library from dry-rb), you should notice that we are accessing a Result object. For those of you who don't know, the Result object can have two forms: success or failure. We can check which one was returned by calling the success? method. It'll return a boolean. It can also carry some data, but that's another topic.
Let's get back to our schema and add some rules. To ensure that a specific key is present, we use the required method. Dry-validation makes a clear distinction between key presence and empty values. This concept is skipped in ActiveModel::Validations
where presence validation fails both when the key is not present and when the value is empty ('', [], {}, nil). Let's say we want to ensure that name and surname are non-empty strings. We can write it as follows.
validator = Dry::Validation.Schema do
required(:name) { filled? && str? }
required(:surname) { filled? && str? }
end
required takes a block with validation logic composed with simple predicates.
You can check out the built-in predicates here https://dry-rb.org/gems/dry-validation/basics/built-in-predicates/
validator.call({
name: 'John',
surname: 123
}).success? #=> false
validator.call({
name: 'John',
surname: 'Doe'
}).success? #=> true
As you can see, we can use logical operators. You are probably wondering why we've used filled? and str? and not just str? alone. str? ensures that the value is of the string type, and filled? checks if the value is not empty. For strings, it must be at least 1 character long, so '' is considered an empty value.
Nested rules
Let's move on to a slightly more advanced topic, which is validating nested data.
Consider the following structure
{
name: 'John',
surname: 'Doe',
contact: {
phone: '+48 123456789',
}
}
As you can see, I've added a nested hash for contact information. Let's say that a phone is required and it must be properly formatted. The email field can be omitted, but if it's present it should also have a valid format.
# these are the simplified regex, don't use them in production!
PHONE_REGEX = /\A\+?[\ \d]*\z/
EMAIL_REGEX = /\A[\w \.]+@[\w \.]+\z/
validator = Dry::Validation.Schema do
required(:name) { filled? && str? }
required(:surname) { filled? && str? }
required(:contact).schema do
required(:phone) { filled? && format?(PHONE_REGEX) }
optional(:email) { filled? && format?(EMAIL_REGEX) }
end
end
Conclusion
Dry-validation is a really flexible and powerful validation library. It's getting more and more popular in the Ruby community. There are many features I haven't covered like error handling, arrays, reusable schemas and more. To learn more, check the official documentation page.