Sure, it’s certainly possible to do this. I’ve done it plenty of times with plain Ruby classes, so dry-struct would just be a shortcut for attribute definition and type-checking.
It’s important to understand the conceptual differences between ActiveModel and Dry-Struct. The Dry system is intended to work in a functional way, with immutable structs. The sort of mutation you typically do with activemodel objects is not supported, by design.
That means validation is an entirely separate step that takes place before instantiating the struct objects. You’ll need to stitch the two processes together in some way, which is easy with dry-monads. See also dry-rails for how you would integrate dry-schema into a Rails controller.
If you’re unfamiliar with all of this, I would recommend taking a piece at a time and solving a single problem with it. It will become clear over time how they work together, but in my experience trying to use it all at once is too hard.
The minimum-viable form object would look something like this:
include Dry::Types(default: :strict)
Username = Strict::String.constrained(min_size: 3)
class Form < Dry::Struct
attribute :username, T::Username