Best practices for using Dry-Types, Dry-Schema, Dry-Validation, and Dry-Struct together in our apps

I am exploring app development with Hanami 2, which has led me to dive into the Dry-rb ecosystem as well. Loving it so far! :heart:

I am having some trouble understanding how to coordinate my usage of the various Dry-rb components to avoid logic duplication in my apps. I would love to hear from others how you are using the Dry-rb ecosystem to model domain objects and logic in your apps.

For example, I like the idea of building up a library of Types to represent all data that comes from outside of the system (user-submitted data, imported data, etc.). No primitives allowed! :face_with_raised_eyebrow:

However, these types can become quite complex when they represent domain objects with a dozen or more attributes. How do you avoid repeating these attributes and their constraints in multiple places?

For example, I think I can define a Hash Type representing a domain object, and then import the Type into a Dry-Struct definition to avoid having to list all of the attributes in two places (I haven’t really had to do this yet, as I’m still relying on ROM auto-structs). Is this a common practice?

What about schemas? I would also like to avoid repeating all of these attributes in a schema that I could use to parse user input params and other external data (that is, to coerce, transform, and validate the params). But inferring a schema from a Hash Type, while possible, is imperfect and can fail in crucial ways. I can provide a code example for this, but I don’t want to clutter up the initial post.

So . . . what are you all doing?

  • How do you organize your Types?
  • How do you re-use them to avoid duplicate logic?
  • Do you use Types in Schemas and Structs, or is this a pipe dream?
  • If you use Hanami, do you define Types for specific slices?
  • In short, what are the best practices?

Thank you for reading and for any and all replies! :green_heart:

This is an example of how Hash Types can fail when used to infer a Schema used to validate params.

Let’s say we have a Contact which can be represented by a Hash with an :id key (an integer) and a :name key (a string). If I wanted to create a Type for this that coerces empty-string values to nil for both keys, it might look like this (please tell me if there is a better way):

Contact = Types::Hash.schema(
  id: Params::Integer.optional.fallback(nil),
  name: Params::String.constrained([:filled]).optional.fallback(nil)
)

This works, although it actually coerces all invalid values to nil. But if I try to use this Type in a Schema, it will fail:

contact_schema = Dry::Schema.Params { required(:contact).filled(Types::Contact) }

contact_schema.(contact: {id: "", name: ""}).inspect

=> "#<Dry::Schema::Result{:contact=>{:id=>nil, :name=>nil}} errors={:contact=>{:id=>[\"must be an integer\"], :name=>[\"must be a string\"]}} path=[]>"

As you can see, the coercion to nil took place, but nil is no longer an accepted value.

If I define my own schema as follows, without using the Type, it works just fine:

contact_schema = Dry::Schema.Params do
  required(:contact).hash do
    required(:id).maybe(:integer)
    required(:name).maybe(:string)
  end  
end

contact_schema.(contact: {id: "", name: ""}).inspect

=> "#<Dry::Schema::Result{:contact=>{:id=>nil, :name=>nil}} errors={} path=[]>"

This is actually better than the Type, because only empty string values are coerced to nil. Other invalid values will produce a validation error.

What am I missing? Am I doing something wrong, or is this just not possible? :face_with_monocle:

Thanks again for reading!

1 Like