Hello,
I am reaching out because I feel confused by the documentation of dry-schema. I do as the docs say but I get errors and I’m stuck.
My case is to have apikey
key in all schemas and some required and optional keys in each individual schema.
According to your documentation it is possible to define a base schema and make individual schemas inherit from it so that the global configuration and rules are applied to each individual schema.
Here what’s your documentation says (dry-rb - dry-schema v1.10 - Working with schemas)
A schema is an object which contains a list of rules that will be applied to its input when you call a schema. It returns a result object which provides an API to retrieve error messages and access to the validation output.
So I define my schema like so:
class AppSchema < Dry::Schema::Params
define do
config.messages.load_paths << '/my/app/config/locales/en.yml'
config.messages.backend = :i18n
required(:apikey).filled(:string)
end
end
# now you can build other schemas on top of the base one:
class MySchema < AppSchema
define do
required(:id).filled(:string)
end
end
Now I would like to verify if the input was correct and if all rules passed. According to your documentation it is possible to do (dry-rb - dry-schema v1.10 - Working with schemas)
However when I call errors
or success?
I get an error.
my_schema = MySchema.new
my_schema.errors
undefined method `errors' for #<MySchema:0x000000013c1b5ae8> (NoMethodError)
my_schema.success?
undefined method `success?' for #<MySchema:0x000000013e998a38> (NoMethodError)
Calling .call
on Schema raises an error
MySchema.call
undefined method `call' for MySchema:Class (NoMethodError)
I am confused. What am I missing?
I think I’ve figured this out. I need to call #call(input)
to validate the schema with given input.
my_schema = MySchema.new(name: 'adasd')
result = my_schema.call({id: 'adasd', name: '123', extra_key: 'asdas'})
result.success? #=> true
result.errors(full: true).to_h
It feels strange for someone used to ActiveRecord
way of validating input.
obj = MyObj.new(input)
obj.valid?
Is it possible to achieve this in dry-schema
or dry-struct
?
You can create a schema and assign it to a constant: Schema = MySchema.new
. Then use it like that:
validation_result = Schema.(input)
if validation_result.success?
# ...
else
# ...
end
There’s a shortcut for defining a schema:
MySchema = Dry::Schema.Params(parent: AppSchema) do
required(:apikey).filled(:string)
# so there's no need to call `MySchema.new` later on
end
Cool, thanks! Very helpful
I’ve just tried it and it seems to work but there are problems with this approach. First problem is that required
field is not shown after calling to_h
even if it is provided.
AppSchema = Dry::Schema.Params do
config.validate_keys = true
required(:id).filled(:string)
end
EventDetails = Dry::Schema.Params(parent: AppSchema) do
optional(:locale).filled(:string)
optional(:domain).filled(:string)
end
input = { apikey: '1234', locale: 'EN', domain: 'example.com'}
puts EventDetails.call(input).to_h
# => {:locale=>"EN", :domain=>"example.com"}
Why apikey
is not there? What am I missing?
Second problem, you can’t define namespaced schemas this way. For instance, I can’t have
# lib/my_module/another_module/my_schema.rb
MyModule::AnotherModule::MySchema = Dry::Schema.Params do
...
end
Is it possible to define schemas using class
- the Ruby way?
I was somehow successsful but I don’t know how
module MyModule
module AnotherModule
class AppSchema < Dry::Schema::Params
define do
config.validate_keys = true
required(:apikey).filled(:string)
end
end
end
end
module MyModule
module AnotherModule
class EventDetails < MyModule::AnotherModule::AppSchema
define do
optional(:locale).filled(:string)
optional(:domain).filled(:string)
end
end
end
end
event_details = MyModule::AnotherModule::EventDetails.new
event_details.call(apikey: ‘1234’, locale: ‘EN’, domain: ‘test’)
It works!
Why apikey
is not there? What am I missing?
First of, it’s :id
in the parent schema, you didn’t define :apikey
hence it isn’t present in the output.
Second, you don’t validation status but you definitely need to do this:
input = { apikey: '1234', locale: 'EN', domain: 'example.com'}
result = EventDetails.call(input)
if result.success?
# then call .to_h
else
# call result.errors
end
To define a nested constant you should
module MyModule
module AnotherModule
MySchema = Dry::Schema.Params do
...
end
end
end
event_details = MyModule::AnotherModule::EventDetails.new
I wouldn’t create a schema on every validation. It’s not free. Your app will be faster if schemas are created once.
1 Like
First of, it’s :id
in the parent schema, you didn’t define :apikey
hence it isn’t present in the output.
Oh, yes, my bad, I overlooked it.
You mean it works faster if you define the schema using a block and call .call
on the constant?
module MyModule
module AnotherModule
MySchema = Dry::Schema.Params do
...
end
end
end
MyModule:: AnotherModule::MySchema.call(input)
rather than using class
keyword?
module MyModule
module AnotherModule
class AppSchema < Dry::Schema::Params
define do
config.validate_keys = true
required(:apikey).filled(:string)
end
end
end
end
schema = MyModule:: AnotherModule:: MySchema.new
result = schema.call(input)
Yeah. I mean I didn’t measure, it’s just there’s no need to create an instance of a schema every time. It’s because we don’t mutate objects in general so the only method that is called on a schema is call
. And it returns a new object with validation results rather than mutating anything.
In my applications, I use DI for defining schemas and validation contracts, they are created, registered in a container, and linked together during the initialization phase. They are also frozen so that I don’t get accidental sharing between requests.
dependency injection, look at dry-auto_inject and dry-system. I also have a sample repo with effects on top of it GitHub - flash-gordon/rt-tracker
Oh, I see. I always use DI in Ruby and I do it like this:
MySchema = Dry::Schema.Params do
...
end
class MyKlass
def initialize(schema: MySchema)
@schema = schema
end
def call(input)
result = @schema.call(input)
...
end
end