PHP version
8.5
duyler/openapi version
0.7.0
OpenAPI spec version
3.0
Description
Parameters declared on a Path Item — paths./x.parameters, which the OpenAPI specification defines as applying to all operations under that path — are parsed into the schema model and then dropped before validation. They are never enforced.
PathItem does hold them:
// src/Schema/Model/PathItem.php:32
public ?Parameters $parameters = null,
But RequestValidator::validate() receives only the Operation and builds its parameter set from that alone:
// src/Validator/Request/RequestValidator.php:37
$parameters = $operation->parameters?->parameters ?? [];
/** @var list<Parameter> $parameterSchemas */
$parameterSchemas = array_filter($parameters, fn($param) => $param instanceof Parameter);
$pathParams = $this->pathParser->matchPath(
$request->getUri()->getPath(),
$pathTemplate,
);
$this->pathParamsValidator->validate($pathParams, $parameterSchemas);
$queryString = $request->getUri()->getQuery();
$queryParams = $this->queryParser->parse($queryString);
$this->queryParamsValidator->validate($queryParams, $parameterSchemas);
The path item's parameters never reach $parameterSchemas, so the same $parameterSchemas is passed to every location validator with the path-level entries missing. RequestValidator is given $pathTemplate but not the PathItem, so it has no way to recover them.
Consequences, all silent:
required: true on a path-level parameter is not enforced — omitting it passes.
schema constraints on path-level parameters (enum, format, minimum, additionalProperties: false, …) are not checked.
- This applies to
in: path as much as in: query. Path parameters are the usual thing to declare at the path item level, since they are shared by every operation on that path, so hoisting them there — exactly as the specification encourages — turns their validation off.
The failure mode is worse than a false rejection: nothing errors, so a spec author has no signal that a whole class of parameters is unvalidated. In our own spec, moving parameters to the path item (the DRY choice for a path with several operations) silently disabled validation for 59 parameter declarations, 45 of them in: path and 46 marked required, and every test still passed.
Per the specification, the merge rule is that path item parameters apply to all operations under the path, and an operation-level parameter with the same name and in overrides the path-level one; the two cannot otherwise duplicate.
Steps to reproduce
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
use Nyholm\Psr7\ServerRequest;
$yaml = <<<'YAML'
openapi: 3.0.0
info:
title: Path Level Parameters API
version: 1.0.0
paths:
/widgets/{widgetId}:
parameters:
- name: widgetId
in: path
required: true
schema:
type: string
format: uuid
- name: mustHave
in: query
required: true
schema:
type: string
enum: [alpha, beta]
get:
responses:
'200':
description: ok
YAML;
$validator = OpenApiValidatorBuilder::create()
->fromYamlString($yaml)
->enableCoercion()
->build();
$cases = [
'required query param omitted ' => '/widgets/2b3e0c4a-59f4-4c4f-9a0a-1e0d9c7f0b11',
'query param violates enum ' => '/widgets/2b3e0c4a-59f4-4c4f-9a0a-1e0d9c7f0b11?mustHave=gamma',
'path param violates format: uuid ' => '/widgets/not-a-uuid?mustHave=alpha',
];
foreach ($cases as $label => $uri) {
$validator->reset();
parse_str((string) parse_url($uri, PHP_URL_QUERY), $query);
$request = new ServerRequest('GET', 'http://localhost' . $uri);
$request = $request->withQueryParams($query);
try {
$validator->validateRequest($request);
printf("ACCEPTED %s %s\n", $label, $uri);
} catch (Throwable $e) {
printf("REJECTED %s %s\n %s\n", $label, $uri, $e->getMessage());
}
}
Actual result
Every violation is accepted:
ACCEPTED required query param omitted /widgets/2b3e0c4a-59f4-4c4f-9a0a-1e0d9c7f0b11
ACCEPTED query param violates enum /widgets/2b3e0c4a-59f4-4c4f-9a0a-1e0d9c7f0b11?mustHave=gamma
ACCEPTED path param violates format: uuid /widgets/not-a-uuid?mustHave=alpha
Expected: all three rejected — a missing required parameter, an enum violation, and a format: uuid violation respectively. Moving both parameters from paths./widgets/{widgetId}.parameters into paths./widgets/{widgetId}.get.parameters makes all three reject as they should, which isolates the path-level declaration as the trigger.
The fix is to merge PathItem.parameters into the operation's set before validation, with operation-level entries overriding path-level ones on matching name + in. Since RequestValidator currently has no access to the PathItem, the merge probably belongs where the operation is resolved from the path item, so that $operation->parameters is already complete by the time it reaches request validation.
PHP version
8.5
duyler/openapi version
0.7.0
OpenAPI spec version
3.0
Description
Parameters declared on a Path Item —
paths./x.parameters, which the OpenAPI specification defines as applying to all operations under that path — are parsed into the schema model and then dropped before validation. They are never enforced.PathItemdoes hold them:But
RequestValidator::validate()receives only theOperationand builds its parameter set from that alone:The path item's parameters never reach
$parameterSchemas, so the same$parameterSchemasis passed to every location validator with the path-level entries missing.RequestValidatoris given$pathTemplatebut not thePathItem, so it has no way to recover them.Consequences, all silent:
required: trueon a path-level parameter is not enforced — omitting it passes.schemaconstraints on path-level parameters (enum,format,minimum,additionalProperties: false, …) are not checked.in: pathas much asin: query. Path parameters are the usual thing to declare at the path item level, since they are shared by every operation on that path, so hoisting them there — exactly as the specification encourages — turns their validation off.The failure mode is worse than a false rejection: nothing errors, so a spec author has no signal that a whole class of parameters is unvalidated. In our own spec, moving parameters to the path item (the DRY choice for a path with several operations) silently disabled validation for 59 parameter declarations, 45 of them
in: pathand 46 markedrequired, and every test still passed.Per the specification, the merge rule is that path item parameters apply to all operations under the path, and an operation-level parameter with the same
nameandinoverrides the path-level one; the two cannot otherwise duplicate.Steps to reproduce
Actual result
Every violation is accepted:
Expected: all three rejected — a missing required parameter, an
enumviolation, and aformat: uuidviolation respectively. Moving both parameters frompaths./widgets/{widgetId}.parametersintopaths./widgets/{widgetId}.get.parametersmakes all three reject as they should, which isolates the path-level declaration as the trigger.The fix is to merge
PathItem.parametersinto the operation's set before validation, with operation-level entries overriding path-level ones on matchingname+in. SinceRequestValidatorcurrently has no access to thePathItem, the merge probably belongs where the operation is resolved from the path item, so that$operation->parametersis already complete by the time it reaches request validation.