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?