Using Dry-Schema/Validation to validate a Model object

Sorta resurrecting Is it possible to use dry-validation to validate ruby objects?

We use validations in a lot of our Command objects, to make sure the kwarg params passed in to #call are what we expect. This is great for validating exactly those args, to act as a “preflight check” and prevent the command from running if the validation fails.

However, what we’re running into more and more often, it we want to not just validate that the object was passed in, but also validate things about the object. Oftentimes, one of the kwargs is a Model object, with attributes that we’d like to validate. For example, we have an Integration model, that has a settings attribute that maps to a jsonb column on Postgres. If we have a command that can only run if some setting is set to true, then we currently have to do something like this:

class CommandContract < Dry::Validation::Contract
  params do
    required(:integration).filled(type?: Integration)
  end

  rule(:integration) do
    key.failure("my_setting is not enabled") unless value.settings["my_setting"]
  end
end

It would “feel” very nice, if instead the validation or schema could match on attributes on the model object, not just values in a hash. Then the above validation might look something like this:

class CommandContract < Dry::Validation::Contract
  params do
    required(:integration).filled(type?: Integration) do
      required(:settings).hash do
        required(:my_setting).value(eql?: true)
      end
    end
  end
end

This would also allow the validation to target the error at the right place/attribute, rather than at some higher level. (“integration settings my_setting must be true” vs “integration my_setting is not enabled”).

I know from that other thread 4 years ago, there’s no plans to support this, but I thought maybe this use-case was different enough to warrant another look. Since we have Dry::Schema.JSON and Dry::Schema.Params, perhaps a Dry::Schema.Attributes or Dry::Schema.Model?

I may be wrong but… what you seems to be trying to do is assert that an specific condition (setting in this case) is satisfied before executing the command. The fact the object/model has a jsonb/hash-like attribute is simply a coincidence (implementation detail) and not a reason for applying validation rules on it (you could/should do it but in another place).

To be honest, the code would look much clearer if this check was placed inside the command and not part of the schema.

class MyCommand
  def call(params, integration)
    return Failure(:disabled_setting) unless integration.my_setting?
  
    # ...

    Success(:ok)
  end
end

That is one solution, if there’s only a single attribute value that needs check. However, one of the advantages of dry-validation is that it’ll check all the validations, and return errors for any that fail. Your example will only fail on the first, and not check the rest.