diff --git a/src/JsonSchema/BackwardCompatibleSchemaFactory.php b/src/JsonSchema/BackwardCompatibleSchemaFactory.php
new file mode 100644
index 00000000000..35b43168b99
--- /dev/null
+++ b/src/JsonSchema/BackwardCompatibleSchemaFactory.php
@@ -0,0 +1,68 @@
+
+ *
+ * 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\JsonSchema;
+
+use ApiPlatform\Metadata\Operation;
+
+/**
+ * This factory decorates range integer and number properties to keep Draft 4 backward compatibility.
+ *
+ * @see https://github.com/api-platform/core/issues/6041
+ *
+ * @internal
+ */
+final class BackwardCompatibleSchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
+{
+ public const SCHEMA_DRAFT4_VERSION = 'draft_4';
+
+ public function __construct(private readonly SchemaFactoryInterface $decorated)
+ {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, Operation $operation = null, Schema $schema = null, array $serializerContext = null, bool $forceCollection = false): Schema
+ {
+ $schema = $this->decorated->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
+
+ if (!($serializerContext[self::SCHEMA_DRAFT4_VERSION] ?? false)) {
+ return $schema;
+ }
+
+ foreach ($schema->getDefinitions() as $definition) {
+ foreach ($definition['properties'] ?? [] as $property) {
+ if (isset($property['type']) && \in_array($property['type'], ['integer', 'number'], true)) {
+ if (isset($property['exclusiveMinimum'])) {
+ $property['minimum'] = $property['exclusiveMinimum'];
+ $property['exclusiveMinimum'] = true;
+ }
+ if (isset($property['exclusiveMaximum'])) {
+ $property['maximum'] = $property['exclusiveMaximum'];
+ $property['exclusiveMaximum'] = true;
+ }
+ }
+ }
+ }
+
+ return $schema;
+ }
+
+ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
+ {
+ if ($this->decorated instanceof SchemaFactoryAwareInterface) {
+ $this->decorated->setSchemaFactory($schemaFactory);
+ }
+ }
+}
diff --git a/src/Symfony/Bundle/Resources/config/json_schema.xml b/src/Symfony/Bundle/Resources/config/json_schema.xml
index 2ad6fe11746..0923e155022 100644
--- a/src/Symfony/Bundle/Resources/config/json_schema.xml
+++ b/src/Symfony/Bundle/Resources/config/json_schema.xml
@@ -36,6 +36,10 @@
+
+
+
+
diff --git a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php
index 590372c5cfd..715ab6d99ef 100644
--- a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php
+++ b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php
@@ -13,6 +13,7 @@
namespace ApiPlatform\Symfony\Bundle\Test;
+use ApiPlatform\JsonSchema\BackwardCompatibleSchemaFactory;
use ApiPlatform\JsonSchema\Schema;
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
use ApiPlatform\Metadata\Get;
@@ -118,7 +119,7 @@ public static function assertMatchesResourceCollectionJsonSchema(string $resourc
$operation = $operationName ? (new GetCollection())->withName($operationName) : new GetCollection();
}
- $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null, $serializationContext);
+ $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null, ($serializationContext ?? []) + [BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION => true]);
static::assertMatchesJsonSchema($schema->getArrayCopy());
}
@@ -133,7 +134,7 @@ public static function assertMatchesResourceItemJsonSchema(string $resourceClass
$operation = $operationName ? (new Get())->withName($operationName) : new Get();
}
- $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null, $serializationContext);
+ $schema = $schemaFactory->buildSchema($resourceClass, $format, Schema::TYPE_OUTPUT, $operation, null, ($serializationContext ?? []) + [BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION => true]);
static::assertMatchesJsonSchema($schema->getArrayCopy());
}
diff --git a/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php b/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php
new file mode 100644
index 00000000000..5394a8eb43e
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/Issue6041/NumericValidated.php
@@ -0,0 +1,74 @@
+
+ *
+ * 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\Issue6041;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Validator\Constraints as Assert;
+
+#[ApiResource(operations: [
+ new Get(uriTemplate: 'numeric-validated/{id}'),
+ new GetCollection(uriTemplate: 'numeric-validated'),
+])]
+#[ORM\Entity]
+class NumericValidated
+{
+ #[ORM\Column(type: 'integer')]
+ #[ORM\Id]
+ #[ORM\GeneratedValue(strategy: 'AUTO')]
+ private ?int $id = null;
+
+ #[Assert\Range(min: 1, max: 10)]
+ #[ORM\Column]
+ public int $range;
+
+ #[Assert\GreaterThan(value: 10)]
+ #[ORM\Column]
+ public int $greaterThanMe;
+
+ #[Assert\GreaterThanOrEqual(value: '10.99')]
+ #[ORM\Column]
+ public float $greaterThanOrEqualToMe;
+
+ #[Assert\LessThan(value: 99)]
+ #[ORM\Column]
+ public int $lessThanMe;
+
+ #[Assert\LessThanOrEqual(value: '99.33')]
+ #[ORM\Column]
+ public float $lessThanOrEqualToMe;
+
+ #[Assert\Positive]
+ #[ORM\Column]
+ public int $positive;
+
+ #[Assert\PositiveOrZero]
+ #[ORM\Column]
+ public int $positiveOrZero;
+
+ #[Assert\Negative]
+ #[ORM\Column]
+ public int $negative;
+
+ #[Assert\NegativeOrZero]
+ #[ORM\Column]
+ public int $negativeOrZero;
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+}
diff --git a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php
index 739c9acbc5e..6e6def4da16 100644
--- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php
+++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php
@@ -17,6 +17,7 @@
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6041\NumericValidated;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\JsonSchemaContextDummy;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\User;
use ApiPlatform\Tests\Fixtures\TestBundle\Model\ResourceInterface;
@@ -180,6 +181,33 @@ public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithCo
$this->assertMatchesResourceCollectionJsonSchema(User::class, null, 'jsonld', ['groups' => ['api-test-case-group']]);
}
+ public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithRangeAssertions(): void
+ {
+ $this->recreateSchema();
+
+ /** @var EntityManagerInterface $manager */
+ $manager = static::getContainer()->get('doctrine')->getManager();
+ $numericValidated = new NumericValidated();
+ $numericValidated->range = 5;
+ $numericValidated->greaterThanMe = 11;
+ $numericValidated->greaterThanOrEqualToMe = 10.99;
+ $numericValidated->lessThanMe = 11;
+ $numericValidated->lessThanOrEqualToMe = 99.33;
+ $numericValidated->positive = 1;
+ $numericValidated->positiveOrZero = 0;
+ $numericValidated->negative = -1;
+ $numericValidated->negativeOrZero = 0;
+
+ $manager->persist($numericValidated);
+ $manager->flush();
+
+ self::createClient()->request('GET', "/numeric-validated/{$numericValidated->getId()}");
+ $this->assertMatchesResourceItemJsonSchema(NumericValidated::class);
+
+ self::createClient()->request('GET', '/numeric-validated');
+ $this->assertMatchesResourceCollectionJsonSchema(NumericValidated::class);
+ }
+
// Next tests have been imported from dms/phpunit-arraysubset-asserts, because the original constraint has been deprecated.
public function testAssertArraySubsetPassesStrictConfig(): void