Convert Dry::Schema::Result to Dry::Struct

Given the following params defined based on dry-validation:

class UserParams < Dry::Validation::Contract
  params do
    required(:email).value(:string)
  end
end

If I want to take email param after validations I have only one choice now - it’s transform to Ruby hash and use [] method:
result = UserParams.new.call(email: ‘user@example.com’)
result[:email] # => ‘user@example.com’

But, If I have a type in the :email key, I will get nil, that’s basically not much better than just take params[:email].

I want Ruby validate my mistakes in the keys. For that reason I would like to get a struct with a strict API after passed validations. By struct here I mean an object has #email method, so that when I have a type on it, I have a clear exception immediately about that.

In short, I want the following code work:

result.email # => 'user@example.com'

But I understand that it would have an obscured API, if the validations don’t pass. For that reason there could be another method that does the transformation into struct:

result.to_struct.email # => 'user@example.com'

to_struct would fail if validations don’t pass.

What do you think if dry-validation implement this method?

Maybe, I’m missing something and it’s already possible to do that transition, but I’ve not found it anywhere. If it’s already there, please point me out. Thanks!

I am no expert, but my understanding is that you need to extract your schema from your validation here. You have different abstractions but you currently all stuff them in your dry-validation so you cannot benefit from them:

  • Your schema
  • Your validation operation
  • Your struct (once the data is validated)

So, define your schema independently.

Use it to create a struct type:

class MyStruct < Dry::Struct
  schema my_schema.strict
end

Now:

  1. Validate your hash with dry-validation & your schema
  2. Now do: result = MyStruct.new(my_validated_hash)

Basically, you were trying to do 2 things in one step: validating & creating your struct. The doc explains why such an approach should be best avoided and why you should separate the two actions.

Thanks for you response! In this code what is my_schema.strict? Is that the schema from dry-validation or a separately defined schema?

You define my_schema independently (first step). Then use it in validation and types.
Is it more clear?

Yes, thanks! It’s not clear though, how to define this schema and reuse inside the validation class. So do I understand correctly that:
1.You suggest to define the schema with dry-schema?
2. It’s possible to reuse this schema inside both places (the struct and the validator)?

Humm
Trying to put together a small example, I realized apis don’t seem to handle that well indeed. I’m not familiar enough with the codebase, but it seems Dry::Struct has nothing built-in to accept a Dry::Schema.

Either my understanding of the abstractions are not on point, or the api could be created. Mainly a bridge between the two representations I guess (although type information is not yet required in dry-schema, which would make the Dry::Struct impossible to build in some cases, which suggests the bridge would not be just easy object transformation).

Someone with more experience with dry-rb should be able to confirm that or not :slight_smile:

Yeah, looking into the codebase I also come up with the idea of implementing a bridge.

I’ve encountered the same problem when using dry-rb with Rails, and resorted to defining both a Dry::Validation::Contract to process the form params, and a Dry::Struct used when rendering the form.

The structure of the Contract schema and Struct are identical.

I end up with the following pattern in the controller:

def new
  @form = PersonStruct.new # includes a #errors method to work with form helpers
end

def create
  @schema = PersonContract.new.call(params[:person].permit!)

  @form = PersonStruct.new(@schema.values)
  @form.errors = @schema.errors.to_h
  @form.errors.default_proc = proc { [] }

  if @schema.success?
    # happy path, perhaps do something with @form or redirect
  else
    render 'new'
  end
end

Having to duplicate the schema on the Contract and the Struct has been one of my main issues using dry-rb with Rails for form processing. I might look into generating the struct automatically.

Could someone familiar with dry-rb internals chime in as to whether this seems like a reasonable use case?

Maybe looking up how Hanami::Model did the trick would be a good hint (don’t have time at the moment myself though :frowning: )

@jviney there’s a huge problem with such a solution - Dry::Struct errors out on invalid input, which means that if an attribute from @schema.values is missing, this line

@form = PersonStruct.new(@schema.values)

will error out. I’ve tried this route myself as well, but the error throwing is quite a problem. The way I did it at the end is to use a regular ruby class with attr_accessor attribute list, but it’s far from ideal.

This is the full implementation of my “form”

require "dry/monads/all"

class ApplicationForm

  # Required dependency for ActiveModel::Errors
  extend ActiveModel::Naming
  extend ActiveModel::Translation

  attr_reader :errors

  def initialize(attributes = {})
    assign_attributes(attributes) if attributes
    @errors = ActiveModel::Errors.new(self)
  end

  def assign_attributes(attributes)
    attributes.each do |key, value|
      setter = :"#{key}="
      public_send(setter, value) if respond_to?(setter)
    end
  end

  # The following methods are needed to be minimally implemented

  def read_attribute_for_validation(attr)
    send(attr)
  end

  class << self
    include Dry::Monads

    def contract(&block)
      @contract = Dry::Validation::Contract.build(&block)
    end

    def validate(params)
      if @contract.nil?
        raise NotImplementedError, <<~STR.strip
          .contract not defined on form. Must be a Dry::Validation::Contract. Example:

          contract do
            params do
              required(:amount).filled(:integer)
            end
          end
        STR
      end

      outcome = @contract.call(params)

      form = new(outcome.to_h)

      if outcome.failure?
        assign_errors_to_form(outcome, form)
        Failure([:validate, form])
      else
        Success(form)
      end
    end

    private

    def assign_errors_to_form(outcome, form)
      msg = build_error_message(outcome.errors)
      form.errors.add(:base, msg)

      outcome.errors.each do |validation_message|
        form.errors.add(validation_message.path[0], validation_message.text, validation_message.meta)
      end
    end

    def build_error_message(errors)
      if errors.any?
        errors.to_h.map { |k, v| "#{k.to_s.humanize}: #{v.first}" }.join("\n")
      else
        # Honeybadger.notify(:validation_error, context: errors)
        "Something went wrong, please refresh the page and try again!"
      end
    end

  end
end

an example of using it:

  class GoalForm < ApplicationForm
    attr_accessor :amount, :title, :plan_id, :preset_id

    contract do
      params do
        required(:plan_id).filled(:string)
        required(:preset_id).filled(:string)
        required(:amount).filled(:integer)
      end
    end
  end
GoalForm.validate(params)

I asked pretty much a similar question a while back:

There’s now an extension, which creates a Validation Schema out of a struct, but not the other way around yet:

I think the lack of good form integration has me rather look back at using Rails for data validation. One way I want to avoid placing validations directly in the model would be subclass the model, and define all validations in the child class. I haven’t tried it yet, but it looks promising.

So, if I have a model

class Goal < ApplicationRecord
  belongs_to :plan
  # attrs: amount, ...
end
class CreateGoal < Goal
 table_name :goals  # or somehow get that from the parent class

 validates_presence_of :amount
 validates_numericality_of :amount
 # .. and so on
end
1 Like

I just stumbled on the dry-rb libraries and it is a little tedious to have to declare the same fields for the struct as well as for the schema. I have a background in Elm so conceptually it makes sense to me that you might have one type for internal representation and another type for converting to/from wire format, but at the same time, the schema doesn’t give you a lot of tools to change the format, for example I don’t even see an easy way to rename a field (except for transform_keys in dry-struct). It seems like this issue indicates that the relationship between the two abstractions is not clear. Should I open a Github issue, do you think?

I think it’s more fair to say that there is no formal relationship between the two abstractions at all, though it would be very useful if there were.