Need to understand Dry::Validation

Hi everyone

I am learning dry-rb , specially Dry::Validations. There are no too many articles showing how to use it with Rails, for example. But for what I can find, including the dry-validation documentation, I see different ways to do what seems to be the same thing. But it’s not clear the real difference in practice.

First thing I would like to understand:

What is the difference between:

class NewUserContract < Dry::Validation::Contract
  params do
    required(:email).filled(:string)
    required(:age).value(:integer)
  end
end

and

class NewUserContract < Dry::Validation::Contract
  json do
    required(:email).filled(:string)
    required(:age).value(:integer)
  end
end

The second, difference I would like to understand is between: filled, value and maybe. Actually, I know that we use maybe when the value maybe nil. But, what about filled vs value ?

Welcome! There isn’t a lot of documentation specific to Rails because Dry-rb is a toolkit intended for all Ruby code. There are a handful of specific Rails integrations provided by dry-rails but those are simply providing a convention for you to follow, it isn’t anything you couldn’t write yourself with only a little effort.

Schema Processor Types

The difference between params and json in your example relates to the schema processor. Ultimately, this mostly just changes the default dry-types that are used.

Schemas in dry-schema process keys in a series of steps:

  1. Key Validation: is a required key missing
  2. Key Coercion: transform a string key to symbol
  3. Value Filter: screen out values before they are coerced
  4. Value Coercion: apply any transformations defined in the type spec
  5. Value Type: does the value pass the type spec
  6. Value predicate: does the predicate (if defined) return true

Validation contracts tack on a final pass, applying rules to the entire schema that can encompass multiple keys.

The reason for params and json processor types comes from the different coercion rules you want to apply depending on whether you are dealing with HTTP params or JSON data.

For instance, it’s commonplace to see empty string returned in HTTP params that params automatically transforms to nil. That doesn’t happen with json. Likewise, JSON defines boolean types so those are not coerced, but HTTP params need to transform string values into booleans. Different rules for different use-cases.

Macros

The distinction between value, filled, and maybe are different macro types.

filled is just a special-case macro for a very common thing you want to do: declare a type but ensure it’s non-empty. I believe I remember reading somewhere this is deprecated, so I would recommend using the predicate instead.

value is the primary macro for a value. It applies the type spec to the value to ensure its correctness, and coerce it if desired.

value(:string) is just a shorthand for value(Dry::Types['strict.string']).

A macro can apply two rules to a value:

  1. type spec
  2. predicate

Predicates come from dry-logic and as you might suspect, they are just predicate methods that answer a question about an object.

So the filled macro is doing two things: a type spec, and a predicate.

value(Dry::Types['strict.string'], :filled?)

So that will ensure that the value is a string, and it is non-empty.

The filled predicate may also be built into the type spec as a constraint using the same logic operation

Types = Dry.Types(default: :strict)

module Types
  FilledString = String.constrained(filled: true)
end

class NewUserContract < Dry::Validation::Contract
  params do
    required(:email).value(Types::FilledString)
    required(:age).value(:integer)
  end
end
2 Likes

Just wanted to clarify that filled is not deprecated and it’s here to stay. This is a very common thing and it streamlines schema definitions pretty significantly. Maybe we’ll move to type-based specs exclusively in a distant future once/if Ruby has a way to express rich types in its core lib.

1 Like