Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generating errors for unexpected keys in Hash #35

Open
backus opened this issue Jun 7, 2016 · 13 comments

Comments

@backus
Copy link

commented Jun 7, 2016

I would love to be able to define a schema which generated errors if it was given unexpected keys:

require 'dry/validation'

schema = Dry::Validation.Schema(strict: true) do
  hash? do
    required(:foo).filled(:int?)
    required(:bar).filled(:str?)
  end
end

outcome = schema.call(foo: 1, bar: "two", baz: 3.0)
p outcome.messages # => {:baz=>["is not allowed"]}

This is really important for defining a public JSON API where you want to reject invalid user input.

@backus

This comment has been minimized.

Copy link
Author

commented Jun 7, 2016

Here is an abridged discussion of this from the gitter chat:

John Backus @backus May 26 12:05

Is there any way to generate an error if a hash has an unexpected key with dry-validation?

Piotr Solnica @solnic May 26 12:53

no, not yet

Piotr Solnica @solnic May 26 12:53

I mean there's no built-in predicate for that

John Backus @backus May 26 12:54

Any way to define it with a custom predicate?
My impression was that you weren't really able to define validations for the entire object being passed to the validator

Piotr Solnica @solnic May 26 13:09

it is possible but with limitations, so you can use hash? do ... end in the root
this will be applied to the input that's passed to validation schema
we can add support for more

Andy Holland @AMHOL May 26 13:10

I ended up with something like:

gemfile(true) { gem 'dry-validation', github: 'dry-rb/dry-validation' }

module MyPredicates
  include Dry::Logic::Predicates

  ALLOWED_KEYS = %i(
    id
    name
    email
  ).freeze

  predicate(:restricted_hash?) do |hash|
    ALLOWED_KEYS & hash.keys == ALLOWED_KEYS
  end
end

schema = Dry::Validation.Schema do
  configure do
    config.predicates = MyPredicates
  end

  restricted_hash? do
    required(:id).filled(:int?)
    required(:name).filled
    required(:email).filled
  end
end

schema.(id: 1, name: 'Joe', email: 'joe@hotmail.com')

That seems to work, if you add the error message

@solnic

This comment has been minimized.

Copy link
Member

commented Jul 2, 2016

We could now easily build this on top of input macro, although I gotta improve it first as the way I implemented it turned out to be problematic when nested schemas are used and error messages are messed up a bit (see #200). Then it's only a matter of extending input to accept more than one predicate and adding a new predicate like restricted_hash? which can just look at rules.keys in schema and that's it.

@qwert321

This comment has been minimized.

Copy link

commented Apr 12, 2017

Thanks.
This is a MUST if dry-validation want to replace strong parameters in rails.
This is a protection from massive assignment in rails.

@fledman

This comment has been minimized.

Copy link

commented May 25, 2017

I don't know how kosher this is, but you can make a custom predicate that dynamically looks up the schema keys, and attach it to the input macro:

require 'dry-validation'

class Base < Dry::Validation::Schema
  def strict_keys?(input)
    (input.keys - self.rules.keys).empty?
  end

  def self.messages
    super.merge(en: { errors: { strict_keys?: 'has unknown keys' } } )
  end
end

schema = Dry::Validation.Schema(Base) do
  input :hash?, :strict_keys?
  required(:foo).filled(:int?)
  optional(:bar).filled(:str?)
end

schema.call(foo: 1, bar: 2)
# => #<Dry::Validation::Result output={:foo=>1, :bar=>2} errors={:bar=>["must be a string"]}>

schema.call(foo: 1, bar: "2")
# => #<Dry::Validation::Result output={:foo=>1, :bar=>"2"} errors={}> 

schema.call(foo: 1, bar: "2", baz: 3)
# => #<Dry::Validation::Result output={:foo=>1, :bar=>"2", :baz=>3} errors=["has unknown keys"]>

schema.call(foo: 1, bar: 2, baz: 3)
# => #<Dry::Validation::Result output={:foo=>1, :bar=>2, :baz=>3} errors=["has unknown keys"]>

the one downside I noticed is that if the input rule fails, it doesn't check the rest of the rules

@fledman

This comment has been minimized.

Copy link

commented May 25, 2017

as an aside, it would be nice if there was a way to pass data from the predicate into the error message e.g. has unknown keys: ["baz"]

@solnic

This comment has been minimized.

Copy link
Member

commented May 25, 2017

This will be supported in 1.0

@pascalbetz

This comment has been minimized.

Copy link

commented Apr 3, 2018

Is there any news on this or should I go with @fledman proposal?

I want to validate parameters that are passed in through an API before I send them to the next system. So i thougt i'd would be nice if I tell the client if he sends params that I don't know instead of just ignoring them with

configure do
  config.input_processor = :sanitizer
end

It's unlikely that unknown parameters are sent, so I'd be OK with not checking the rest of the rules.

Thanks dry-rb team for the good work.

@fledman

This comment has been minimized.

Copy link

commented Apr 18, 2018

@pascalbetz
you can hack together a fake check (what the high level rules use) which prevents early stopping and adds a specific error message for each unknown key.

But it is super dirty & highly dependent on the current internal api.

expand for terrible hack
require 'dry-validation'

module StrictKeys
  def self.name
    :__strict_keys__
  end
  def self.to_ast
    [:type, self]
  end
  def self.rule
    @rule ||= Checker.new
  end
  def self.failure(key)
    Dry::Logic::Result.new(false, key) do
      [:key, [key, [:predicate, [name, []]]]]
    end
  end
  class Checker < Dry::Validation::Guard
    def initialize; end
    def with(*); self; end
    def call(input, results)
      input.each do |k,v|
        results[k] = StrictKeys.failure(k) unless results.key?(k)
      end
      nil
    end
  end
end

class Base < Dry::Validation::Schema
  def self.messages
    super.merge(en: { errors: { StrictKeys.name => 'is an unknown key' } } )
  end
end

schema = Dry::Validation.Schema(Base) do
  checks << StrictKeys
  required(:foo).filled(:int?)
  optional(:bar).filled(:str?)
end
@pascalbetz

This comment has been minimized.

Copy link

commented Apr 19, 2018

@fledman thanks for the explanation. I'm in the process of refactoring the APIs and see if/how I can make use of your proposal.

@solnic solnic transferred this issue from dry-rb/dry-validation Feb 8, 2019

@joelvh

This comment has been minimized.

Copy link

commented May 17, 2019

Will Schema#strict be implemented in dry-validation v1.0.0 Contract as a feature, or do we need to key into the underlying schema?

@solnic

This comment has been minimized.

Copy link
Member

commented May 17, 2019

@joelvh I'd prefer to have it as part of dry-schema but we'll see what makes more sense.

@joelvh

This comment has been minimized.

Copy link

commented May 17, 2019

@solnic that was quick! I just realized that Schema#strict is in dry-types, which dry-schema and dry-validation don't (?) use under the hood?

Lots of things to reconcile getting things upgraded to v1.0.0 - happy to see many of the changes!

@solnic

This comment has been minimized.

Copy link
Member

commented May 17, 2019

@solnic that was quick!

You commented while I was going through my inbox 😄

I just realized that Schema#strict is in dry-types, which dry-schema and dry-validation don't (?) use under the hood?

Strict hash schemas from dry-types are not used at all by dry-schema/validation. In order to validate input's keys, we need support for "base" errors. This was added to dry-validation so now dry-schema needs to catch up.

Lots of things to reconcile getting things upgraded to v1.0.0 - happy to see many of the changes!

Yes it's a big step forward with dry-types/logic/schema/validation reaching 1.0.0 😄

@solnic solnic added this to the 1.4.0 milestone Jul 10, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.