Repository: api-platform/core (php)

Issue:
HeaderParameter and validation not coherent
**API Platform version(s) affected**: 3.4 -> 4.1.x
tested with my project (3.4) and validated that it still exist with master of api-platform/api-platform so I assume 4.x

**Description**  
`HeaderParameter` constraint seems not coherent. Here, I tested the validation of an header parameter for the api. It seems to be inconsistent.
* checking that it's required : worked great
* checking that my value is less or equals than X : work great on the openapi front part, not on back part (no validation)
* checking that my value is an integer : work great on the openapi front part, not on back part
* checking that my value is greater or equals than X  : failed

**How to reproduce**  

I took the demo project, and on the greeting entity I have make this change

`#[ApiResource(mercure: true)]`
to
`#[ApiResource(mercure: true, parameters: ['Foo' => new HeaderParameter(schema: ['type' => 'integer', 'example' => 3, 'minimum' => 1, 'maximum' => 5], required: true)])]`

to check each test, maximum need to be removed to test the other constraint

**Possible Solution**  
validation on the value of the array ?

**Additional Context**  
Perhaps I missuses the `HeaderParameter` ? But the output in openApi format seems coherent 

The constraint is applied on an array (don't know why an array instead of the value), so perhaps it's the reason of the behaviour?

thanks


Pull request:
refactor(metadata): type parameters to list<string>|string
| Q             | A
| ------------- | ---
| Branch?       | 4.1
| Tickets       | Closes #7111
| License       | MIT

Adds a nativeType to parameters, defaults to `list<string>|string` as this is the default with Symfony query parameters parsing and for HTTP Headers. It also fixes the value extraction accordingly and the validation that may have work partially on a value that's extracted as an array.

TODO:

- [x] fix doctrine filters


Patch:
diff --git a/composer.json b/composer.json
index 88f98e460..b85f01b00 100644
--- a/composer.json
+++ b/composer.json
@@ -114,6 +114,7 @@
         "symfony/property-info": "^6.4 || ^7.1",
         "symfony/serializer": "^6.4 || ^7.0",
         "symfony/translation-contracts": "^3.3",
+        "symfony/type-info": "^7.2",
         "symfony/web-link": "^6.4 || ^7.1",
         "willdurand/negotiation": "^3.1"
     },
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 4b3474879..d8527fddf 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -38,6 +38,7 @@
       <directory>.</directory>
     </include>
     <exclude>
+      <directory>tests</directory>
       <directory>features</directory>
       <directory>vendor</directory>
       <file>.php-cs-fixer.dist.php</file>
diff --git a/src/Doctrine/Common/composer.json b/src/Doctrine/Common/composer.json
index 2b663d6f4..cfa972c09 100644
--- a/src/Doctrine/Common/composer.json
+++ b/src/Doctrine/Common/composer.json
@@ -25,7 +25,7 @@
     "require": {
         "php": ">=8.2",
         "api-platform/metadata": "^4.1",
-        "api-platform/state": "^4.1",
+        "api-platform/state": "^4.1.8",
         "doctrine/collections": "^2.1",
         "doctrine/common": "^3.2.2",
         "doctrine/persistence": "^3.2"
diff --git a/src/Doctrine/Odm/composer.json b/src/Doctrine/Odm/composer.json
index 814c9283c..dc1c2553e 100644
--- a/src/Doctrine/Odm/composer.json
+++ b/src/Doctrine/Odm/composer.json
@@ -27,7 +27,7 @@
         "php": ">=8.2",
         "api-platform/doctrine-common": "^4.1",
         "api-platform/metadata": "^4.1",
-        "api-platform/state": "^4.1",
+        "api-platform/state": "^4.1.8",
         "doctrine/mongodb-odm": "^2.10",
         "symfony/property-info": "^6.4 || ^7.1"
     },
diff --git a/src/Doctrine/Orm/composer.json b/src/Doctrine/Orm/composer.json
index 6b7c9bd80..6972438cf 100644
--- a/src/Doctrine/Orm/composer.json
+++ b/src/Doctrine/Orm/composer.json
@@ -26,7 +26,7 @@
         "php": ">=8.2",
         "api-platform/doctrine-common": "^4.1",
         "api-platform/metadata": "^4.1",
-        "api-platform/state": "^4.1",
+        "api-platform/state": "^4.1.8",
         "doctrine/orm": "^2.17 || ^3.0",
         "symfony/property-info": "^6.4 || ^7.1"
     },
diff --git a/src/Laravel/Eloquent/Extension/FilterQueryExtension.php b/src/Laravel/Eloquent/Extension/FilterQueryExtension.php
index dc91528cb..c28ceaaaf 100644
--- a/src/Laravel/Eloquent/Extension/FilterQueryExtension.php
+++ b/src/Laravel/Eloquent/Extension/FilterQueryExtension.php
@@ -53,6 +53,11 @@ final readonly class FilterQueryExtension implements QueryExtensionInterface
                 continue;
             }
 
+            // most eloquent filters work with only a single value
+            if (\is_array($values) && array_is_list($values) && 1 === \count($values)) {
+                $values = current($values);
+            }
+
             $filter = $filterId instanceof FilterInterface ? $filterId : ($this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null);
             if ($filter instanceof FilterInterface) {
                 $builder = $filter->apply($builder, $values, $parameter, $context + ($parameter->getFilterContext() ?? []));
diff --git a/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php b/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php
index c6ffd33f9..afa2e1067 100644
--- a/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php
+++ b/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php
@@ -28,6 +28,12 @@ final readonly class SortFilterParameterProvider implements ParameterProviderInt
         $parameters = $operation->getParameters();
         $properties = $parameter->getExtraProperties()['_properties'] ?? [];
         $value = $parameter->getValue();
+
+        // most eloquent filters work with only a single value
+        if (\is_array($value) && array_is_list($value) && 1 === \count($value)) {
+            $value = current($value);
+        }
+
         if (!\is_string($value)) {
             return $operation;
         }
diff --git a/src/Laravel/Exception/ErrorHandler.php b/src/Laravel/Exception/ErrorHandler.php
index 7dcc70277..f496c5d1e 100644
--- a/src/Laravel/Exception/ErrorHandler.php
+++ b/src/Laravel/Exception/ErrorHandler.php
@@ -15,6 +15,7 @@ namespace ApiPlatform\Laravel\Exception;
 
 use ApiPlatform\Laravel\ApiResource\Error;
 use ApiPlatform\Laravel\Controller\ApiPlatformController;
+use ApiPlatform\Metadata\Exception\InvalidUriVariableException;
 use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
 use ApiPlatform\Metadata\Exception\StatusAwareExceptionInterface;
 use ApiPlatform\Metadata\HttpOperation;
@@ -192,7 +193,7 @@ class ErrorHandler extends ExceptionsHandler
             return $exception->getStatusCode();
         }
 
-        if ($exception instanceof RequestExceptionInterface) {
+        if ($exception instanceof RequestExceptionInterface || $exception instanceof InvalidUriVariableException) {
             return 400;
         }
 
diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php
index 3e36ff5f8..ef83fee28 100644
--- a/src/Metadata/Extractor/XmlResourceExtractor.php
+++ b/src/Metadata/Extractor/XmlResourceExtractor.php
@@ -301,7 +301,7 @@ final class XmlResourceExtractor extends AbstractResourceExtractor
                 continue;
             }
 
-            $data[(string) $cacheHeader['name']] = (string) $cacheHeader;
+            $data[(string) $cacheHeader['name']] = XmlUtils::phpize((string) $cacheHeader);
         }
 
         return $data;
diff --git a/src/Metadata/Parameter.php b/src/Metadata/Parameter.php
index 1acef3d8b..6fdd9ff98 100644
--- a/src/Metadata/Parameter.php
+++ b/src/Metadata/Parameter.php
@@ -16,6 +16,7 @@ namespace ApiPlatform\Metadata;
 use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
 use ApiPlatform\State\ParameterNotFound;
 use ApiPlatform\State\ParameterProviderInterface;
+use Symfony\Component\TypeInfo\Type;
 
 /**
  * @experimental
@@ -29,6 +30,7 @@ abstract class Parameter
      * @param list<string>                                                       $properties      a list of properties this parameter applies to (works with the :property placeholder)
      * @param FilterInterface|string|null                                        $filter
      * @param mixed                                                              $constraints     an array of Symfony constraints, or an array of Laravel rules
+     * @param Type                                                               $nativeType      the PHP native type, we cast values to an array if its a CollectionType, if not and it's an array with a single value we use it (eg: HTTP Header)
      */
     public function __construct(
         protected ?string $key = null,
@@ -47,6 +49,7 @@ abstract class Parameter
         protected ?string $securityMessage = null,
         protected ?array $extraProperties = [],
         protected array|string|null $filterContext = null,
+        protected ?Type $nativeType = null,
     ) {
     }
 
@@ -296,4 +299,17 @@ abstract class Parameter
 
         return $self;
     }
+
+    public function getNativeType(): ?Type
+    {
+        return $this->nativeType;
+    }
+
+    public function withNativeType(Type $nativeType): self
+    {
+        $self = clone $this;
+        $self->nativeType = $nativeType;
+
+        return $self;
+    }
 }
diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php
index d4a6416f7..767548776 100644
--- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php
+++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php
@@ -32,6 +32,7 @@ use ApiPlatform\State\Util\StateOptionsTrait;
 use Psr\Container\ContainerInterface;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
+use Symfony\Component\TypeInfo\Type;
 
 /**
  * Prepares Parameters documentation by reading its filter details and declaring an OpenApi parameter.
@@ -148,6 +149,16 @@ final class ParameterResourceMetadataCollectionFactory implements ResourceMetada
             }
 
             $parameter = $this->setDefaults($key, $parameter, $resourceClass, $properties, $operation);
+            // We don't do any type cast yet, a query parameter or an header is always a string or a list of strings
+            if (null === $parameter->getNativeType()) {
+                // this forces the type to be only a list
+                if ('array' === ($parameter->getSchema()['type'] ?? null)) {
+                    $parameter = $parameter->withNativeType(Type::list(Type::string()));
+                } else {
+                    $parameter = $parameter->withNativeType(Type::union(Type::string(), Type::list(Type::string())));
+                }
+            }
+
             $priority = $parameter->getPriority() ?? $internalPriority--;
             $parameters->add($key, $parameter->withPriority($priority));
         }
diff --git a/src/Metadata/composer.json b/src/Metadata/composer.json
index 63ae4d7a4..5366e43c1 100644
--- a/src/Metadata/composer.json
+++ b/src/Metadata/composer.json
@@ -33,7 +33,7 @@
         "psr/log": "^1.0 || ^2.0 || ^3.0",
         "symfony/property-info": "^6.4 || ^7.1",
         "symfony/string": "^6.4 || ^7.0",
-        "symfony/type-info": "^7.1"
+        "symfony/type-info": "^7.2"
     },
     "require-dev": {
         "api-platform/json-schema": "^4.1",
diff --git a/src/State/Util/ParameterParserTrait.php b/src/State/Util/ParameterParserTrait.php
index 4d67397c3..052823134 100644
--- a/src/State/Util/ParameterParserTrait.php
+++ b/src/State/Util/ParameterParserTrait.php
@@ -13,10 +13,13 @@ declare(strict_types=1);
 
 namespace ApiPlatform\State\Util;
 
+use ApiPlatform\Metadata\HeaderParameter;
 use ApiPlatform\Metadata\HeaderParameterInterface;
 use ApiPlatform\Metadata\Parameter;
 use ApiPlatform\State\ParameterNotFound;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\UnionType;
 
 /**
  * @internal
@@ -64,11 +67,7 @@ trait ParameterParserTrait
         }
 
         $value = $values[$key] ?? new ParameterNotFound();
-        if (!$accessors) {
-            return $value;
-        }
-
-        foreach ($accessors as $accessor) {
+        foreach ($accessors ?? [] as $accessor) {
             if (\is_array($value) && isset($value[$accessor])) {
                 $value = $value[$accessor];
             } else {
@@ -77,6 +76,28 @@ trait ParameterParserTrait
             }
         }
 
+        if ($value instanceof ParameterNotFound) {
+            return $value;
+        }
+
+        $isCollectionType = fn ($t) => $t instanceof CollectionType;
+        $isCollection = $parameter->getNativeType()?->isSatisfiedBy($isCollectionType) ?? false;
+
+        // type-info 7.2
+        if (!$isCollection && $parameter->getNativeType() instanceof UnionType) {
+            foreach ($parameter->getNativeType()->getTypes() as $t) {
+                if ($isCollection = $t->isSatisfiedBy($isCollectionType)) {
+                    break;
+                }
+            }
+        }
+
+        if ($isCollection) {
+            $value = \is_array($value) ? $value : [$value];
+        } elseif ($parameter instanceof HeaderParameter && \is_array($value) && array_is_list($value) && 1 === \count($value)) {
+            $value = $value[0];
+        }
+
         return $value;
     }
 }
diff --git a/src/Symfony/EventListener/ErrorListener.php b/src/Symfony/EventListener/ErrorListener.php
index 6f5bf6eba..19e5593a3 100644
--- a/src/Symfony/EventListener/ErrorListener.php
+++ b/src/Symfony/EventListener/ErrorListener.php
@@ -15,6 +15,7 @@ namespace ApiPlatform\Symfony\EventListener;
 
 use ApiPlatform\Metadata\Error as ErrorOperation;
 use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
+use ApiPlatform\Metadata\Exception\InvalidUriVariableException;
 use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
 use ApiPlatform\Metadata\HttpOperation;
 use ApiPlatform\Metadata\IdentifiersExtractorInterface;
@@ -178,7 +179,7 @@ final class ErrorListener extends SymfonyErrorListener
             return $exception->getStatusCode();
         }
 
-        if ($exception instanceof RequestExceptionInterface) {
+        if ($exception instanceof RequestExceptionInterface || $exception instanceof InvalidUriVariableException) {
             return 400;
         }
 
diff --git a/src/Symfony/Validator/State/ParameterValidatorProvider.php b/src/Symfony/Validator/State/ParameterValidatorProvider.php
index 85e702380..741c4292e 100644
--- a/src/Symfony/Validator/State/ParameterValidatorProvider.php
+++ b/src/Symfony/Validator/State/ParameterValidatorProvider.php
@@ -62,14 +62,7 @@ final class ParameterValidatorProvider implements ProviderInterface
                 $value = null;
             }
 
-            $violations = [];
-            if (\is_array($value) && $properties = $parameter->getExtraProperties()['_properties'] ?? []) {
-                foreach ($properties as $property) {
-                    $violations = [...$violations, ...$this->validator->validate($value[$property] ?? null, $constraints)];
-                }
-            } else {
-                $violations = $this->validator->validate($value, $constraints);
-            }
+            $violations = $this->validator->validate($value, $constraints);
 
             foreach ($violations as $violation) {
                 $constraintViolationList->add(new ConstraintViolation(
@@ -108,7 +101,7 @@ final class ParameterValidatorProvider implements ProviderInterface
         }
 
         if ($p = $violation->getPropertyPath()) {
-            return $p;
+            return $key.$p;
         }
 
         return $key;
diff --git a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php
index e34124f08..fd6191dfe 100644
--- a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php
+++ b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php
@@ -21,7 +21,11 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInter
 use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
 use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
 use Psr\Container\ContainerInterface;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\UnionType;
+use Symfony\Component\Validator\Constraints\All;
 use Symfony\Component\Validator\Constraints\Choice;
+use Symfony\Component\Validator\Constraints\Collection;
 use Symfony\Component\Validator\Constraints\Count;
 use Symfony\Component\Validator\Constraints\DivisibleBy;
 use Symfony\Component\Validator\Constraints\GreaterThan;
@@ -31,6 +35,7 @@ use Symfony\Component\Validator\Constraints\LessThan;
 use Symfony\Component\Validator\Constraints\LessThanOrEqual;
 use Symfony\Component\Validator\Constraints\NotBlank;
 use Symfony\Component\Validator\Constraints\NotNull;
+use Symfony\Component\Validator\Constraints\Range;
 use Symfony\Component\Validator\Constraints\Regex;
 use Symfony\Component\Validator\Constraints\Type;
 use Symfony\Component\Validator\Constraints\Unique;
@@ -108,55 +113,82 @@ final class ParameterValidationResourceMetadataCollectionFactory implements Reso
         }
 
         $assertions = [];
+        $allowEmptyValue = $openApi?->getAllowEmptyValue();
+        if (false === ($allowEmptyValue ?? $openApi?->getAllowEmptyValue())) {
+            $assertions[] = new NotBlank(allowNull: !$required);
+        }
 
-        if ($required && false !== ($allowEmptyValue = $openApi?->getAllowEmptyValue())) {
-            $assertions[] = new NotNull(message: \sprintf('The parameter "%s" is required.', $parameter->getKey()));
+        $minimum = $schema['exclusiveMinimum'] ?? $schema['minimum'] ?? null;
+        $exclusiveMinimum = isset($schema['exclusiveMinimum']);
+        $maximum = $schema['exclusiveMaximum'] ?? $schema['maximum'] ?? null;
+        $exclusiveMaximum = isset($schema['exclusiveMaximum']);
+
+        if ($minimum && $maximum) {
+            if (!$exclusiveMinimum && !$exclusiveMaximum) {
+                $assertions[] = new Range(min: $minimum, max: $maximum);
+            } else {
+                $assertions[] = $exclusiveMinimum ? new GreaterThan(value: $minimum) : new GreaterThanOrEqual(value: $minimum);
+                $assertions[] = $exclusiveMaximum ? new LessThan(value: $maximum) : new LessThanOrEqual(value: $maximum);
+            }
+        } elseif ($minimum) {
+            $assertions[] = $exclusiveMinimum ? new GreaterThan(value: $minimum) : new GreaterThanOrEqual(value: $minimum);
+        } elseif ($maximum) {
+            $assertions[] = $exclusiveMaximum ? new LessThan(value: $maximum) : new LessThanOrEqual(value: $maximum);
         }
 
-        if (false === ($allowEmptyValue ?? $openApi?->getAllowEmptyValue())) {
-            $assertions[] = new NotBlank(allowNull: !$required);
+        if (isset($schema['pattern'])) {
+            $assertions[] = new Regex('#'.$schema['pattern'].'#');
         }
 
-        if (isset($schema['exclusiveMinimum'])) {
-            $assertions[] = new GreaterThan(value: $schema['exclusiveMinimum']);
+        if (isset($schema['maxLength']) || isset($schema['minLength'])) {
+            $assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null);
         }
 
-        if (isset($schema['exclusiveMaximum'])) {
-            $assertions[] = new LessThan(value: $schema['exclusiveMaximum']);
+        if (isset($schema['multipleOf'])) {
+            $assertions[] = new DivisibleBy(value: $schema['multipleOf']);
+        }
+
+        if (isset($schema['enum'])) {
+            $assertions[] = new Choice(choices: $schema['enum']);
         }
 
-        if (isset($schema['minimum'])) {
-            $assertions[] = new GreaterThanOrEqual(value: $schema['minimum']);
+        if ($properties = $parameter->getExtraProperties()['_properties'] ?? []) {
+            $fields = [];
+            foreach ($properties as $propertyName) {
+                $fields[$propertyName] = $assertions;
+            }
+
+            return $parameter->withConstraints(new Collection(fields: $fields, allowMissingFields: true));
         }
 
-        if (isset($schema['maximum'])) {
-            $assertions[] = new LessThanOrEqual(value: $schema['maximum']);
+        $isCollectionType = fn ($t) => $t instanceof CollectionType;
+        $isCollection = $parameter->getNativeType()?->isSatisfiedBy($isCollectionType) ?? false;
+
+        // type-info 7.2
+        if (!$isCollection && $parameter->getNativeType() instanceof UnionType) {
+            foreach ($parameter->getNativeType()->getTypes() as $t) {
+                if ($isCollection = $t->isSatisfiedBy($isCollectionType)) {
+                    break;
+                }
+            }
         }
 
-        if (isset($schema['pattern'])) {
-            $assertions[] = new Regex('#'.$schema['pattern'].'#');
+        if ($isCollection) {
+            $assertions = $assertions ? [new All($assertions)] : [];
         }
 
-        if (isset($schema['maxLength']) || isset($schema['minLength'])) {
-            $assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null);
+        if ($required && false !== $allowEmptyValue) {
+            $assertions[] = new NotNull(message: \sprintf('The parameter "%s" is required.', $parameter->getKey()));
         }
 
         if (isset($schema['minItems']) || isset($schema['maxItems'])) {
             $assertions[] = new Count(min: $schema['minItems'] ?? null, max: $schema['maxItems'] ?? null);
         }
 
-        if (isset($schema['multipleOf'])) {
-            $assertions[] = new DivisibleBy(value: $schema['multipleOf']);
-        }
-
         if ($schema['uniqueItems'] ?? false) {
             $assertions[] = new Unique();
         }
 
-        if (isset($schema['enum'])) {
-            $assertions[] = new Choice(choices: $schema['enum']);
-        }
-
         if (isset($schema['type']) && 'array' === $schema['type']) {
             $assertions[] = new Type(type: 'array');
         }
diff --git a/src/Validator/composer.json b/src/Validator/composer.json
index cded96d24..d473960fb 100644
--- a/src/Validator/composer.json
+++ b/src/Validator/composer.json
@@ -24,6 +24,7 @@
     "require": {
         "php": ">=8.2",
         "api-platform/metadata": "^4.1",
+        "symfony/type-info": "^7.2",
         "symfony/web-link": "^6.4 || ^7.1"
     },
     "require-dev": {


Test patch:
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php b/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php
index 83ef08293..71a6792ba 100644
--- a/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue6673/MutlipleParameterProvider.php
@@ -17,6 +17,8 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\Metadata\Parameter;
 use ApiPlatform\Metadata\QueryParameter;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 
 #[GetCollection(
     uriTemplate: 'issue6673_multiple_parameter_provider',
@@ -25,9 +27,11 @@ use ApiPlatform\Metadata\QueryParameter;
     parameters: [
         'a' => new QueryParameter(
             provider: [self::class, 'parameterOneProvider'],
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
         ),
         'b' => new QueryParameter(
             provider: [self::class, 'parameterTwoProvider'],
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
         ),
     ],
     provider: [self::class, 'provide']
diff --git a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php
index aaf74caae..9c29ed33e 100644
--- a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php
+++ b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php
@@ -27,6 +27,8 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\Serializer\Attribute\Groups;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 use Symfony\Component\Validator\Constraints as Assert;
 
 #[Get(
@@ -58,14 +60,37 @@ use Symfony\Component\Validator\Constraints as Assert;
     uriTemplate: 'validate_parameters{._format}',
     parameters: [
         'enum' => new QueryParameter(schema: ['enum' => ['a', 'b'], 'uniqueItems' => true]),
-        'num' => new QueryParameter(schema: ['minimum' => 1, 'maximum' => 3]),
-        'exclusiveNum' => new QueryParameter(schema: ['exclusiveMinimum' => 1, 'exclusiveMaximum' => 3]),
-        'blank' => new QueryParameter(openApi: new OpenApiParameter(name: 'blank', in: 'query', allowEmptyValue: false)),
-        'length' => new QueryParameter(schema: ['maxLength' => 1, 'minLength' => 3]),
+        'num' => new QueryParameter(
+            schema: ['minimum' => 1, 'maximum' => 3],
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
+        'exclusiveNum' => new QueryParameter(
+            schema: ['exclusiveMinimum' => 1, 'exclusiveMaximum' => 3],
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
+        'blank' => new QueryParameter(
+            openApi: new OpenApiParameter(name: 'blank', in: 'query', allowEmptyValue: false),
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
+        'length' => new QueryParameter(
+            schema: ['maxLength' => 1, 'minLength' => 3],
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
         'array' => new QueryParameter(schema: ['minItems' => 2, 'maxItems' => 3]),
-        'multipleOf' => new QueryParameter(schema: ['multipleOf' => 2]),
-        'int' => new QueryParameter(property: 'a', constraints: [new Assert\Type('integer')], provider: [self::class, 'toInt']),
-        'pattern' => new QueryParameter(schema: ['pattern' => '\d']),
+        'multipleOf' => new QueryParameter(
+            schema: ['multipleOf' => 2],
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
+        'int' => new QueryParameter(
+            property: 'a',
+            constraints: [new Assert\Type('integer')],
+            provider: [self::class, 'toInt'],
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
+        'pattern' => new QueryParameter(
+            schema: ['pattern' => '\d'],
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
     ],
     provider: [self::class, 'collectionProvider']
 )]
@@ -77,7 +102,16 @@ use Symfony\Component\Validator\Constraints as Assert;
 )]
 #[GetCollection(
     uriTemplate: 'with_parameters_header_and_query{._format}',
-    parameters: new Parameters([new QueryParameter(key: 'q'), new HeaderParameter(key: 'q')]),
+    parameters: new Parameters([
+        new QueryParameter(
+            key: 'q',
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
+        new HeaderParameter(
+            key: 'q',
+            nativeType: new BuiltinType(TypeIdentifier::STRING),
+        ),
+    ]),
     provider: [self::class, 'headerAndQueryProvider']
 )]
 #[GetCollection(
@@ -87,6 +121,21 @@ use Symfony\Component\Validator\Constraints as Assert;
     ],
     provider: [self::class, 'headerProvider']
 )]
+#[GetCollection(
+    uriTemplate: 'header_integer',
+    parameters: [
+        'Foo' => new HeaderParameter(
+            schema: [
+                'type' => 'integer',
+                'example' => 3,
+                'minimum' => 1,
+                'maximum' => 5,
+            ],
+            required: true,
+        ),
+    ],
+    provider: [self::class, 'noopProvider']
+)]
 #[QueryParameter(key: 'everywhere')]
 class WithParameter
 {
@@ -170,4 +219,9 @@ class WithParameter
 
         return new JsonResponse($values);
     }
+
+    public static function noopProvider(Operation $operation, array $uriVariables = [], array $context = []): JsonResponse
+    {
+        return new JsonResponse([]);
+    }
 }
diff --git a/tests/Fixtures/TestBundle/ApiResource/WithSecurityParameter.php b/tests/Fixtures/TestBundle/ApiResource/WithSecurityParameter.php
index e1084e673..4707c359f 100644
--- a/tests/Fixtures/TestBundle/ApiResource/WithSecurityParameter.php
+++ b/tests/Fixtures/TestBundle/ApiResource/WithSecurityParameter.php
@@ -16,13 +16,15 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\HeaderParameter;
 use ApiPlatform\Metadata\QueryParameter;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 
 #[GetCollection(
     uriTemplate: 'with_security_parameters_collection{._format}',
     parameters: [
         'name' => new QueryParameter(security: 'is_granted("ROLE_ADMIN")'),
-        'auth' => new HeaderParameter(security: '"secured" == auth[0]'),
-        'secret' => new QueryParameter(security: '"secured" == secret'),
+        'auth' => new HeaderParameter(security: '"secured" == auth', nativeType: new BuiltinType(TypeIdentifier::STRING)),
+        'secret' => new QueryParameter(security: '"secured" == secret', nativeType: new BuiltinType(TypeIdentifier::STRING)),
     ],
     provider: [self::class, 'collectionProvider'],
 )]
diff --git a/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php b/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php
index efa2428f8..8964c3d49 100644
--- a/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php
+++ b/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php
@@ -18,16 +18,20 @@ use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\QueryParameter;
 use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 
 #[ApiResource]
 #[GetCollection(
     parameters: [
         'active' => new QueryParameter(
             filter: new BooleanFilter(),
+            nativeType: new BuiltinType(TypeIdentifier::BOOL),
         ),
         'enabled' => new QueryParameter(
             filter: new BooleanFilter(),
             property: 'active',
+            nativeType: new BuiltinType(TypeIdentifier::BOOL),
         ),
     ],
 )]
diff --git a/tests/Fixtures/TestBundle/Document/FilteredExistsParameter.php b/tests/Fixtures/TestBundle/Document/FilteredExistsParameter.php
index ee9812fe9..9ff9e628c 100644
--- a/tests/Fixtures/TestBundle/Document/FilteredExistsParameter.php
+++ b/tests/Fixtures/TestBundle/Document/FilteredExistsParameter.php
@@ -18,6 +18,8 @@ use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\QueryParameter;
 use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 
 #[ApiResource]
 #[GetCollection(
@@ -25,10 +27,12 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
     parameters: [
         'createdAt' => new QueryParameter(
             filter: new ExistsFilter(),
+            nativeType: new BuiltinType(TypeIdentifier::BOOL),
         ),
         'hasCreationDate' => new QueryParameter(
             filter: new ExistsFilter(),
             property: 'createdAt',
+            nativeType: new BuiltinType(TypeIdentifier::BOOL),
         ),
         'exists[:property]' => new QueryParameter(
             filter: new ExistsFilter(),
diff --git a/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php b/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php
index d3328acca..ea6e5538d 100644
--- a/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php
+++ b/tests/Fixtures/TestBundle/Document/FilteredOrderParameter.php
@@ -19,6 +19,8 @@ use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\QueryParameter;
 use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 
 #[ApiResource]
 #[GetCollection(
@@ -26,19 +28,23 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
     parameters: [
         'createdAt' => new QueryParameter(
             filter: new OrderFilter(),
+            nativeType: new BuiltinType(TypeIdentifier::STRING)
         ),
         'date' => new QueryParameter(
             filter: new OrderFilter(),
             property: 'createdAt',
+            nativeType: new BuiltinType(TypeIdentifier::STRING)
         ),
         'date_null_always_first' => new QueryParameter(
             filter: new OrderFilter(),
             property: 'createdAt',
             filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST,
+            nativeType: new BuiltinType(TypeIdentifier::STRING)
         ),
         'date_null_always_first_old_way' => new QueryParameter(
             filter: new OrderFilter(properties: ['createdAt' => OrderFilterInterface::NULLS_ALWAYS_FIRST]),
             property: 'createdAt',
+            nativeType: new BuiltinType(TypeIdentifier::STRING)
         ),
         'order[:property]' => new QueryParameter(
             filter: new OrderFilter(),
diff --git a/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php
index 6ba24650a..259c2aafa 100644
--- a/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php
+++ b/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php
@@ -18,16 +18,20 @@ use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\QueryParameter;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 
 #[ApiResource]
 #[GetCollection(
     parameters: [
         'active' => new QueryParameter(
             filter: new BooleanFilter(),
+            nativeType: new BuiltinType(TypeIdentifier::BOOL),
         ),
         'enabled' => new QueryParameter(
             filter: new BooleanFilter(),
             property: 'active',
+            nativeType: new BuiltinType(TypeIdentifier::BOOL),
         ),
     ],
 )]
diff --git a/tests/Fixtures/TestBundle/Entity/FilteredExistsParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredExistsParameter.php
index c154f9798..e04d55a1f 100644
--- a/tests/Fixtures/TestBundle/Entity/FilteredExistsParameter.php
+++ b/tests/Fixtures/TestBundle/Entity/FilteredExistsParameter.php
@@ -18,6 +18,8 @@ use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\QueryParameter;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 
 #[ApiResource]
 #[GetCollection(
@@ -25,10 +27,12 @@ use Doctrine\ORM\Mapping as ORM;
     parameters: [
         'createdAt' => new QueryParameter(
             filter: new ExistsFilter(),
+            nativeType: new BuiltinType(TypeIdentifier::BOOL),
         ),
         'hasCreationDate' => new QueryParameter(
             filter: new ExistsFilter(),
             property: 'createdAt',
+            nativeType: new BuiltinType(TypeIdentifier::BOOL),
         ),
         'exists[:property]' => new QueryParameter(
             filter: new ExistsFilter(),
diff --git a/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php
index 3da02eeb7..f431cb67b 100644
--- a/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php
+++ b/tests/Fixtures/TestBundle/Entity/FilteredOrderParameter.php
@@ -19,6 +19,8 @@ use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\QueryParameter;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
 
 #[ApiResource]
 #[GetCollection(
@@ -26,19 +28,23 @@ use Doctrine\ORM\Mapping as ORM;
     parameters: [
         'createdAt' => new QueryParameter(
             filter: new OrderFilter(),
+            nativeType: new BuiltinType(TypeIdentifier::STRING)
         ),
         'date' => new QueryParameter(
             filter: new OrderFilter(),
             property: 'createdAt',
+            nativeType: new BuiltinType(TypeIdentifier::STRING)
         ),
         'date_null_always_first' => new QueryParameter(
             filter: new OrderFilter(),
             property: 'createdAt',
             filterContext: OrderFilterInterface::NULLS_ALWAYS_FIRST,
+            nativeType: new BuiltinType(TypeIdentifier::STRING)
         ),
         'date_null_always_first_old_way' => new QueryParameter(
             filter: new OrderFilter(properties: ['createdAt' => OrderFilterInterface::NULLS_ALWAYS_FIRST]),
             property: 'createdAt',
+            nativeType: new BuiltinType(TypeIdentifier::STRING)
         ),
         'order[:property]' => new QueryParameter(
             filter: new OrderFilter(),
diff --git a/tests/Fixtures/TestBundle/Entity/Issue7135/Bar.php b/tests/Fixtures/TestBundle/Entity/Issue7135/Bar.php
new file mode 100644
index 000000000..c762b92c7
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/Issue7135/Bar.php
@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas <dunglas@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7135;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Uid\Uuid;
+
+#[ORM\Entity()]
+#[ApiResource(
+    shortName: 'BarPr7135',
+    operations: [
+        new Get(
+            uriTemplate: '/pull-request-7135/bar/{id}',
+        ),
+    ]
+)]
+#[ORM\Table(name: 'bar6466')]
+class Bar
+{
+    #[ORM\Id]
+    #[ORM\Column(type: 'symfony_uuid', unique: true)]
+    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
+    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
+    public Uuid $id;
+
+    #[ORM\Column]
+    public string $title = '';
+
+    public function __construct(?Uuid $id = null)
+    {
+        $this->id = $id ?: Uuid::v7();
+    }
+}
diff --git a/tests/Fixtures/TestBundle/Entity/Issue7135/Foo.php b/tests/Fixtures/TestBundle/Entity/Issue7135/Foo.php
new file mode 100644
index 000000000..b45378e50
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/Issue7135/Foo.php
@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas <dunglas@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7135;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Post;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Uid\Uuid;
+
+#[ORM\Entity()]
+#[ApiResource(
+    shortName: 'FooPr7135',
+    operations: [
+        new Post(
+            uriTemplate: '/pull-request-7135/foo/',
+        ),
+    ],
+    normalizationContext: ['iri_only' => true],
+)]
+#[ORM\Table(name: 'foo6466')]
+class Foo
+{
+    #[ORM\Id]
+    #[ORM\Column(type: 'symfony_uuid', unique: true)]
+    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
+    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
+    public Uuid $id;
+
+    #[ORM\ManyToOne(targetEntity: Bar::class)]
+    #[ORM\JoinColumn(referencedColumnName: 'id', nullable: false)]
+    public Bar $bar;
+
+    public function __construct()
+    {
+        $this->id = Uuid::v7();
+    }
+}
diff --git a/tests/Functional/Issues/Issue7135Test.php b/tests/Functional/Issues/Issue7135Test.php
new file mode 100644
index 000000000..22aa5c310
--- /dev/null
+++ b/tests/Functional/Issues/Issue7135Test.php
@@ -0,0 +1,121 @@
+<?php
+
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas <dunglas@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Issues;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7135\Bar;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7135\Foo;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Tools\SchemaTool;
+use Symfony\Component\Uid\Uuid;
+
+class Issue7135Test extends ApiTestCase
+{
+    use RecreateSchemaTrait;
+    use SetupClassResourcesTrait;
+
+    protected static ?bool $alwaysBootKernel = false;
+
+    /**
+     * @return class-string[]
+     */
+    public static function getResources(): array
+    {
+        return [Bar::class, Foo::class];
+    }
+
+    public function testValidPostRequestWithIriWhenIdentifierIsUuid(): void
+    {
+        $container = static::getContainer();
+        if ('mongodb' === $container->getParameter('kernel.environment')) {
+            $this->markTestSkipped();
+        }
+
+        $this->recreateSchema(self::getResources());
+        $bar = $this->loadBarFixture();
+
+        $response = self::createClient()->request('POST', '/pull-request-7135/foo/', [
+            'json' => [
+                'bar' => 'pull-request-7135/bar/'.$bar->id,
+            ],
+        ]);
+
+        self::assertEquals(201, $response->getStatusCode());
+    }
+
+    public function testInvalidPostRequestWithIriWhenIdentifierIsUuid(): void
+    {
+        $container = static::getContainer();
+        if ('mongodb' === $container->getParameter('kernel.environment')) {
+            $this->markTestSkipped();
+        }
+
+        $response = self::createClient()->request('POST', '/pull-request-7135/foo/', [
+            'json' => [
+                'bar' => 'pull-request-7135/bar/invalid-uuid',
+            ],
+        ]);
+
+        self::assertEquals(400, $response->getStatusCode());
+        self::assertJsonContains(['detail' => 'Identifier "id" could not be transformed.']);
+    }
+
+    public function testInvalidGetRequestWhenIdentifierIsUuid(): void
+    {
+        $container = static::getContainer();
+        if ('mongodb' === $container->getParameter('kernel.environment')) {
+            $this->markTestSkipped();
+        }
+
+        $response = self::createClient()->request('GET', '/pull-request-7135/bar/invalid-uuid');
+
+        self::assertEquals(404, $response->getStatusCode());
+    }
+
+    protected function loadBarFixture(): Bar
+    {
+        $container = static::getContainer();
+        $registry = $container->get('doctrine');
+        $manager = $registry->getManager();
+
+        $bar = new Bar(Uuid::fromString('0196b66f-66bd-780b-95fe-0ce987a32357'));
+        $bar->title = 'Bar one';
+        $manager->persist($bar);
+
+        $manager->flush();
+
+        return $bar;
+    }
+
+    protected function tearDown(): void
+    {
+        $container = static::getContainer();
+        $registry = $container->get('doctrine');
+        $manager = $registry->getManager();
+        if (!$manager instanceof EntityManagerInterface) {
+            return;
+        }
+
+        $classes = [];
+        foreach (self::getResources() as $entityClass) {
+            $classes[] = $manager->getClassMetadata($entityClass);
+        }
+
+        $schemaTool = new SchemaTool($manager);
+        @$schemaTool->dropSchema($classes);
+        parent::tearDown();
+    }
+}
diff --git a/tests/Functional/Parameters/ParameterTest.php b/tests/Functional/Parameters/ParameterTest.php
index 6620975d9..5d08e514a 100644
--- a/tests/Functional/Parameters/ParameterTest.php
+++ b/tests/Functional/Parameters/ParameterTest.php
@@ -16,6 +16,7 @@ namespace ApiPlatform\Tests\Functional\Parameters;
 use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
 use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter;
 use ApiPlatform\Tests\SetupClassResourcesTrait;
+use PHPUnit\Framework\Attributes\DataProvider;
 
 final class ParameterTest extends ApiTestCase
 {
@@ -96,7 +97,7 @@ final class ParameterTest extends ApiTestCase
     {
         $response = self::createClient()->request('GET', 'with_parameters_header_and_query?q=blabla', ['headers' => ['q' => '(complex stuff)']]);
         $this->assertEquals($response->toArray(), [
-            ['(complex stuff)'],
+            '(complex stuff)',
             'blabla',
         ]);
     }
@@ -109,4 +110,19 @@ final class ParameterTest extends ApiTestCase
         self::createClient()->request('GET', 'header_required', ['headers' => []]);
         $this->assertResponseStatusCodeSame(422);
     }
+
+    #[DataProvider('provideHeaderValues')]
+    public function testHeaderParameterInteger(string $value, int $expectedStatusCode): void
+    {
+        self::createClient()->request('GET', 'header_integer', ['headers' => ['Foo' => $value]]);
+        $this->assertResponseStatusCodeSame($expectedStatusCode);
+    }
+
+    public static function provideHeaderValues(): iterable
+    {
+        yield 'valid integer' => ['3', 200];
+        yield 'too high' => ['6', 422];
+        yield 'too low' => ['0', 422];
+        yield 'invalid integer' => ['string', 422];
+    }
 }
diff --git a/tests/Functional/Parameters/ValidationTest.php b/tests/Functional/Parameters/ValidationTest.php
index f93d07982..73b2752df 100644
--- a/tests/Functional/Parameters/ValidationTest.php
+++ b/tests/Functional/Parameters/ValidationTest.php
@@ -59,10 +59,13 @@ final class ValidationTest extends ApiTestCase
                 'enum[]=c&enum[]=c',
                 [
                     [
-                        'message' => 'This collection should contain only unique elements.',
+                        'propertyPath' => 'enum[0]', 'message' => 'The value you selected is not a valid choice.',
+                    ],
+                    [
+                        'propertyPath' => 'enum[1]', 'message' => 'The value you selected is not a valid choice.',
                     ],
                     [
-                        'propertyPath' => 'enum', 'message' => 'The value you selected is not a valid choice.',
+                        'message' => 'This collection should contain only unique elements.',
                     ],
                 ],
             ],
@@ -101,7 +104,7 @@ final class ValidationTest extends ApiTestCase
             [
                 'num=5',
                 [
-                    ['propertyPath' => 'num', 'message' => 'This value should be less than or equal to 3.'],
+                    ['propertyPath' => 'num', 'message' => 'This value should be between 1 and 3.'],
                 ],
             ],
             [