undefined method error for my schema

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 :+1:

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 :thinking:

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! :slight_smile:

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.

DI? What does it mean? :thinking:

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