diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d782466bd6..77701f7b230 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
## 2.7.0
+* JSON Schema: Add support for generating property schema with Unique restriction (#4159)
* **BC**: Change `api_platform.listener.request.add_format` priority from 7 to 28 to execute it before firewall (priority 8) (#3599)
* **BC**: Use `@final` annotation in ORM filters (#4109)
* Allow defining `exception_to_status` per operation (#3519)
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/validator.xml b/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
index 77455124011..ab4a914c91a 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/validator.xml
@@ -34,6 +34,10 @@
+
+
+
+
diff --git a/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestriction.php b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestriction.php
new file mode 100644
index 00000000000..8a7a0359669
--- /dev/null
+++ b/src/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestriction.php
@@ -0,0 +1,40 @@
+
+ *
+ * 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\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction;
+
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\Unique;
+
+/**
+ * @author Tomas Norkūnas
+ */
+final class PropertySchemaUniqueRestriction implements PropertySchemaRestrictionMetadataInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function create(Constraint $constraint, PropertyMetadata $propertyMetadata): array
+ {
+ return ['uniqueItems' => true];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Constraint $constraint, PropertyMetadata $propertyMetadata): bool
+ {
+ return $constraint instanceof Unique;
+ }
+}
diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
index 4a9c065f3b9..362d4da9d28 100644
--- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
+++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php
@@ -1339,6 +1339,7 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar
'api_platform.metadata.property_schema.one_of_restriction',
'api_platform.metadata.property_schema.regex_restriction',
'api_platform.metadata.property_schema.format_restriction',
+ 'api_platform.metadata.property_schema.unique_restriction',
'api_platform.metadata.property.metadata_factory.yaml',
'api_platform.metadata.property.name_collection_factory.yaml',
'api_platform.metadata.resource.filter_metadata_factory.annotation',
diff --git a/tests/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestrictionTest.php b/tests/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestrictionTest.php
new file mode 100644
index 00000000000..43f8f8b49f0
--- /dev/null
+++ b/tests/Bridge/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestrictionTest.php
@@ -0,0 +1,57 @@
+
+ *
+ * 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\Core\Tests\Bridge\Symfony\Validator\Metadata\Property\Restriction;
+
+use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaUniqueRestriction;
+use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
+use ApiPlatform\Core\Tests\ProphecyTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\Positive;
+use Symfony\Component\Validator\Constraints\Unique;
+
+/**
+ * @author Tomas Norkūnas
+ */
+final class PropertySchemaUniqueRestrictionTest extends TestCase
+{
+ use ProphecyTrait;
+
+ private $propertySchemaUniqueRestriction;
+
+ protected function setUp(): void
+ {
+ $this->propertySchemaUniqueRestriction = new PropertySchemaUniqueRestriction();
+ }
+
+ /**
+ * @dataProvider supportsProvider
+ */
+ public function testSupports(Constraint $constraint, PropertyMetadata $propertyMetadata, bool $expectedResult): void
+ {
+ self::assertSame($expectedResult, $this->propertySchemaUniqueRestriction->supports($constraint, $propertyMetadata));
+ }
+
+ public function supportsProvider(): \Generator
+ {
+ yield 'supported' => [new Unique(), new PropertyMetadata(), true];
+
+ yield 'not supported' => [new Positive(), new PropertyMetadata(), false];
+ }
+
+ public function testCreate(): void
+ {
+ self::assertSame(['uniqueItems' => true], $this->propertySchemaUniqueRestriction->create(new Unique(), new PropertyMetadata()));
+ }
+}
diff --git a/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php b/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php
index f7c59d07666..3b8118a0456 100644
--- a/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php
+++ b/tests/Bridge/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php
@@ -17,12 +17,14 @@
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaLengthRestriction;
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaOneOfRestriction;
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRegexRestriction;
+use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaUniqueRestriction;
use ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\ValidatorPropertyMetadataFactory;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Tests\Fixtures\DummyAtLeastOneOfValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyIriWithValidationEntity;
use ApiPlatform\Core\Tests\Fixtures\DummySequentiallyValidatedEntity;
+use ApiPlatform\Core\Tests\Fixtures\DummyUniqueValidatedEntity;
use ApiPlatform\Core\Tests\Fixtures\DummyValidatedEntity;
use ApiPlatform\Core\Tests\ProphecyTrait;
use Doctrine\Common\Annotations\AnnotationReader;
@@ -406,4 +408,30 @@ public function testCreateWithAtLeastOneOfConstraint(): void
['minLength' => 10],
], $schema['oneOf']);
}
+
+ public function testCreateWithPropertyUniqueRestriction(): void
+ {
+ $validatorClassMetadata = new ClassMetadata(DummyUniqueValidatedEntity::class);
+ (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata);
+
+ $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class);
+ $validatorMetadataFactory->getMetadataFor(DummyUniqueValidatedEntity::class)
+ ->willReturn($validatorClassMetadata)
+ ->shouldBeCalled();
+
+ $decoratedPropertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
+ $decoratedPropertyMetadataFactory->create(DummyUniqueValidatedEntity::class, 'dummyItems', [])->willReturn(
+ new PropertyMetadata()
+ )->shouldBeCalled();
+
+ $validationPropertyMetadataFactory = new ValidatorPropertyMetadataFactory(
+ $validatorMetadataFactory->reveal(),
+ $decoratedPropertyMetadataFactory->reveal(),
+ [new PropertySchemaUniqueRestriction()]
+ );
+
+ $schema = $validationPropertyMetadataFactory->create(DummyUniqueValidatedEntity::class, 'dummyItems')->getSchema();
+
+ $this->assertSame(['uniqueItems' => true], $schema);
+ }
}
diff --git a/tests/Fixtures/DummyUniqueValidatedEntity.php b/tests/Fixtures/DummyUniqueValidatedEntity.php
new file mode 100644
index 00000000000..bfb23be28c6
--- /dev/null
+++ b/tests/Fixtures/DummyUniqueValidatedEntity.php
@@ -0,0 +1,26 @@
+
+ *
+ * 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\Core\Tests\Fixtures;
+
+use Symfony\Component\Validator\Constraints as Assert;
+
+class DummyUniqueValidatedEntity
+{
+ /**
+ * @var string[]
+ *
+ * @Assert\Unique
+ */
+ public $dummyItems;
+}