We're very happy to announce the release of dry-validation 1.0.0!
This is a big release: it includes a rewritten schema DSL, released as dry-schema, and a completely redesigned validation system. If you're interested to know the reasoning behind these changes, please refer to the "Plans for dry-validation + dry-schema (a new gem!)" post on our forum. Yes, it's from February 2017, this took a while, but it was totally worth the wait. Continue reading to see why.
New-old schema DSL
The schema DSL has been rewritten from scratch and not only did it fix dozens of known issues, it also introduced a couple of new features. That said, some complex features that didn't fit anymore were removed. In dry-validation 1.0.0, the schema DSL is delegated to dry-schema and you can still define 3 types of schemas:
schema
- a plain schema that does not perform any coercionsparams
- a schema with coercions optimized for HTTP paramsjson
- a schema with coercions optimized for JSON
The syntax for defining keys with validations is almost identicial to the one you know from previous versions of dry-validation. However, there's a big conceptual difference between those earlier versions and how dry-validation 1.0.0 is intended to be used now.
Contracts with rules
We have a completely new concept called Contract
that allows you to define a schema and domain validation rules. The new rule system is completely decoupled from the schema validation, but it's still type-safe, which means that when you define a rule you can assume the types of the values are correct. This removes the need to perform any additional checks in validation rules and you are going to love this.
Here's a simple example where we define a contract for a new user data:
class NewUserContract < Dry::Validation::Contract
params do
required(:name).filled(:string)
required(:age).filled(:integer)
end
rule(:name) do
key.failure("is too short") if value.length < 3
end
rule(:age) do
key.failure("you must be at least 13 years old") if value < 13
end
end
If you are familiar with the old version, your immediate reaction might be "oh that's more code, why not just define these checks in the schema?" That's a good question to ask! It's still possible to use all the known predicates so, technically speaking, you could perform these checks via the schema but it's not recommended. Starting with 1.0.0, we're moving to a new way of thinking about validations by splitting them into basic structural and type checks handled by schemas and domain validations handled by contracts and their rules. This is a good way of separating concerns to make your code cleaner, simpler and more reusable.
If the amount of code you need to write is a concern, don't worry, because we have a new macro system in place to DRY things up.
Improved messages
One of the biggest limitations in the previous version was the way you could provide custom error messages. Starting from 1.0.0, you have complete control over this process. You can now:
- Provide a message as a plain string, e.g.
key.failure("oops this is wrong")
- Provide a message using a locale identifier, e.g.
key.failure(:invalid)
- Pass extra data when using locales, e.g.
key.failure(:invalid, more: "info", goes: "here")
- Pass additional metadata in addition to the message text, e.g.
key.failure(text: "oops this is wrong", code: :red)
On top of this, we still support localized backends using plain YAML
or I18n
gem.
Base messages
Another nice improvement is support for base messages. This means you can provide a message that will be associated with the whole input, instead of a specific key.
Here's an example:
class EventContract < Dry::Validation::Contract
option :today, default: Date.method(:today)
params do
required(:start_date).value(:date)
required(:end_date).value(:date)
end
rule do
if today.saturday? || today.sunday?
base.failure('creating events is allowed only on weekdays')
end
end
end
Now we can access base errors (assuming it's a weekend):
contract = EventContract.new
contract.call(start_date: Date.today+1, end_date: Date.today+2).errors
# #<Dry::Validation::MessageSet
# messages=[
# #<Dry::Validation::Message text="creating events is allowed only on weekdays" path=[nil] meta={}>
# ]
# options={}
# >
Macros
As mentioned above, you can use the new macro system to reduce code duplication. Currently, there's only one built-in macro, called :acceptance
, but we'll be adding more.
Here's an example how you could use the :acceptance
macro:
class NewUserContract < Dry::Validation::Contract
schema do
required(:email).filled(:string)
required(:terms).filled(:bool)
end
rule(:terms).validate(:acceptance)
end
contract = NewUserContract.new
contract.call(email: "jane@doe.org", terms: "false").errors.to_h
# => {:terms=>["must accept terms"]}
contract.call(email: "jane@doe.org", terms: "true").errors.to_h
# => {}
Defining your own macros is very simple and you're encouraged to do so. Let's say we want to encapsulate checking if a string is of a minimum length, here's how you could do it with a macro:
class ApplicationContract < Dry::Validation::Contract
register_macro(:min_length) do |macro:|
key.failure("is too short") if value.length < macro.args[0]
end
end
Now we can use our :min_length
macro in other contract classes:
class NewUserContract < ApplicationContract
schema do
required(:email).filled(:string)
required(:password).filled(:string)
end
rule(:password).validate(min_length: 12)
end
contract = NewUserContract.new
contract.call(email: "jane@doe.org", terms: "false", password: "secret").errors.to_h
# => {:password=>["is too short"]}
The posibilities are endless and I'm sure we'll soon have a nice collection of macros either built into the main gem or provided as an extension.
Improved validation of array elements
Validating array elements can be tricky business, but it's become nice and simple in dry-validation 1.0.0. It works using the same mechanism as other value types - an array element will not be checked by a rule unless the corresponding schema checks passed.
To validate array elements, use Rule#each
:
class NewSongContract < Dry::Validation::Contract
params do
required(:artist).filled(:string)
required(:title).filled(:string)
required(:tags).array(:string)
end
rule(:tags).each do
key.failure("tag length must be at least 3") if value.length < 3
end
end
Now let's see it in action:
contract = NewSongContract.new
contract.(artist: "Queen", title: "Bohemian Rhapsody", tags: ["rock", 123, "ab"]).errors.to_h
# => {:tags=>{1=>["must be a string"], 2=>["tag length must be at least 3"]}
Notice that our rule did not crash on 123
value even though Integer
does not implement length
- instead, we got a nice error that the second element must be a string. This is how type safety in rules work.
Upgrading from dry-validation 0.x
Please refer to the comprehensive guide "dry-rb 1.0: upgrading validations, types and schemas," written by Igor Morozov. He's done a terrific job explaining the process.
Additionally, check out:
If you need help with upgrading, please do not hesitate to ask questions either on our discussion forum or community chat.
Thank you
Thank you to all the contributors and early adopters who helped us shape dry-validation. This has been a big effort and we're very happy with the results. Please check it out and let us know what you think!