From 9ebb2d508ac021b835e0136615e6df6823d7f100 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Tue, 10 Nov 2026 07:45:56 +0100 Subject: [PATCH] Validate the params of an optional Hash scope without the iterator A validator reads its scope's params directly, and skips the attributes iterator, when its scope is always validated: the root scope and the required Hash scopes under it. A Hash scope that is optional, or that sits under an optional one, went through the iterator instead, although it has nothing to add there either: #validate only goes on once the scope's params are there and not blank, so the iterator hands that one Hash back, has no empty value to pass over and no dependency to meet. The direct path now covers every scope with no Array scope and no `given` on its chain. It no longer tries a required Hash scope inside an Array scope, whose params resolve to an Array and were resolved a second time by the iterator. Co-Authored-By: Claude Fable 5.2 --- CHANGELOG.md | 1 + lib/grape/validations/validators/base.rb | 18 ++++++++++-------- .../validators/default_validator.rb | 9 +++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 879f1f918..dabfb17a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ * [#3071](https://github.com/ruby-grape/grape/pull/3071): Skip the query parse when building the params of a request with no query string - [@ericproulx](https://github.com/ericproulx). * [#3072](https://github.com/ruby-grape/grape/pull/3072): Answer the `Accept` lists axios, jQuery and HTTPie send without negotiating them on every request - [@ericproulx](https://github.com/ericproulx). * [#3073](https://github.com/ruby-grape/grape/pull/3073): Let a `rescue_from :all` in a namespace or mounted API outrank the enclosing scope's handlers - [@ericproulx](https://github.com/ericproulx). +* [#3075](https://github.com/ruby-grape/grape/pull/3075): Validate the params of an optional Hash scope without the attributes iterator - [@ericproulx](https://github.com/ericproulx). * [#3076](https://github.com/ruby-grape/grape/pull/3076): Skip the query merge when building the params of a body sent without a query string - [@ericproulx](https://github.com/ericproulx). * Your contribution here. diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 63db6377d..e20a94fa0 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -121,17 +121,19 @@ def validate_param!(attr_name, params) # time a validator is built. See #validate and #validate!. def choose_validation_path(scope) @always_validated = scope.always_validated? - @direct = @always_validated && !scope.iterates_elements? + @direct = scope.independent? && scope.array_depth.zero? @direct_elements = scope.validated_when_given? && scope.iterates_elements? && scope.array_depth == 1 end - # #validate! on a scope that always validates and does not iterate - # elements, once its params resolved to a Hash: the root scope and the - # required Hash scopes under it, which is most validators of most - # endpoints. There the iterator hands back that Hash alone, and every - # scope on the chain is required with no dependency, so the checks - # around the attribute loop drop out. The machinery cost more than - # the validation itself. + # #validate! on a scope with no Array scope and no +given+ on its + # chain, once its params resolved to a Hash: the root scope and the + # Hash scopes under it, required or optional, which is most validators + # of most endpoints. There the iterator hands back that Hash alone, + # and no scope on the chain has a dependency to meet. An optional one + # is only passed over when its params are empty, and #validate does + # not get this far with those (see ParamsScope#should_validate?). So + # the checks around the attribute loop drop out. The machinery cost + # more than the validation itself. def validate_attributes!(params) array_errors = nil diff --git a/lib/grape/validations/validators/default_validator.rb b/lib/grape/validations/validators/default_validator.rb index d918d946e..6625adc4e 100644 --- a/lib/grape/validations/validators/default_validator.rb +++ b/lib/grape/validations/validators/default_validator.rb @@ -31,10 +31,11 @@ def validate!(params) private # Also #validate!'s whole job where Base#validate_attributes! applies: - # a scope that is always validated and does not iterate elements, once - # its params resolved to a Hash -- the root scope of every +params+ - # block with a default in it. A scope like that depends on no other - # param, so the iterator and its dependency check have nothing to add. + # a scope with no Array scope and no +given+ on its chain, once its + # params resolved to a Hash -- the root scope of every +params+ block + # with a default in it, and the Hash scopes under it. A scope like + # that depends on no other param, so the iterator and its dependency + # check have nothing to add. def apply_defaults(params) @attrs.each do |attr_name| params[attr_name] = @default_call.call(params) if params[attr_name].nil?