diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 097eaddd4b8..e853087d397 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -88,6 +88,19 @@ public function buildSchema(string $className, string $format = 'json', string $ return $schema; } + $isJsonMergePatch = 'json' === $format && 'PATCH' === $operation->getMethod() && Schema::TYPE_INPUT === $type; + $skipRequiredProperties = false; + + if ($isJsonMergePatch) { + if (null === ($skipRequiredProperties = $operation->getExtraProperties()['patch_skip_schema_required_properties'] ?? null)) { + trigger_deprecation('api-platform/core', '3.4', "Set 'patch_skip_schema_required_properties' on extra properties as API Platform 4 will remove required properties from the JSON Schema on Patch request."); + } + + if (true === $skipRequiredProperties) { + $definitionName .= self::PATCH_SCHEMA_POSTFIX; + } + } + if (!isset($schema['$ref']) && !isset($schema['type'])) { $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName; if ($forceCollection || ('POST' !== $method && $operation instanceof CollectionOperationInterface)) { @@ -129,6 +142,7 @@ public function buildSchema(string $className, string $format = 'json', string $ } $options = ['schema_type' => $type] + $this->getFactoryOptions($serializerContext, $validationGroups, $operation instanceof HttpOperation ? $operation : null); + foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options); if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) { @@ -136,7 +150,7 @@ public function buildSchema(string $className, string $format = 'json', string $ } $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName; - if ($propertyMetadata->isRequired()) { + if ($propertyMetadata->isRequired() && !$skipRequiredProperties) { $definition['required'][] = $normalizedPropertyName; } diff --git a/tests/Fixtures/TestBundle/ApiResource/PatchRequired/PatchMe.php b/tests/Fixtures/TestBundle/ApiResource/PatchRequired/PatchMe.php new file mode 100644 index 00000000000..a5573da4856 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/PatchRequired/PatchMe.php @@ -0,0 +1,30 @@ + + * + * 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\ApiResource\PatchRequired; + +use ApiPlatform\Metadata\Patch; +use Symfony\Component\Validator\Constraints\NotNull; + +#[Patch(uriTemplate: '/patch_required_stuff', provider: [self::class, 'provide'])] +final class PatchMe +{ + public ?string $a = null; + #[NotNull] + public ?string $b = null; + + public static function provide(): self + { + return new self(); + } +} diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index ed68a69e50e..b65fce63480 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DocumentDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Tester\ApplicationTester; @@ -24,6 +25,7 @@ */ class JsonSchemaGenerateCommandTest extends KernelTestCase { + use ExpectDeprecationTrait; private ApplicationTester $tester; private string $entityClass; @@ -76,9 +78,9 @@ public function testExecuteWithJsonldTypeInput(): void $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_post', '--format' => 'jsonld', '--type' => 'input']); $result = $this->tester->getDisplay(); - $this->assertStringContainsString('@id', $result); - $this->assertStringContainsString('@context', $result); - $this->assertStringContainsString('@type', $result); + $this->assertStringNotContainsString('@id', $result); + $this->assertStringNotContainsString('@context', $result); + $this->assertStringNotContainsString('@type', $result); } /** @@ -103,24 +105,24 @@ public function testArraySchemaWithReference(): void $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); - $this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['tests'], [ + $this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['tests'], [ 'type' => 'string', 'foo' => 'bar', ]); - $this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['nonResourceTests'], [ + $this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['nonResourceTests'], [ 'type' => 'array', 'items' => [ - '$ref' => '#/definitions/NonResourceTestEntity.jsonld-write.input', + '$ref' => '#/definitions/NonResourceTestEntity.jsonld-write', ], ]); - $this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['description'], [ + $this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['description'], [ 'maxLength' => 255, ]); - $this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['type'], [ - '$ref' => '#/definitions/TestEntity.jsonld-write.input', + $this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['type'], [ + '$ref' => '#/definitions/TestEntity.jsonld-write', ]); } @@ -130,14 +132,14 @@ public function testArraySchemaWithMultipleUnionTypesJsonLd(): void $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); - $this->assertEquals($json['definitions']['Nest.jsonld.output']['properties']['owner']['anyOf'], [ - ['$ref' => '#/definitions/Wren.jsonld.output'], - ['$ref' => '#/definitions/Robin.jsonld.output'], + $this->assertEquals($json['definitions']['Nest.jsonld']['properties']['owner']['anyOf'], [ + ['$ref' => '#/definitions/Wren.jsonld'], + ['$ref' => '#/definitions/Robin.jsonld'], ['type' => 'null'], ]); - $this->assertArrayHasKey('Wren.jsonld.output', $json['definitions']); - $this->assertArrayHasKey('Robin.jsonld.output', $json['definitions']); + $this->assertArrayHasKey('Wren.jsonld', $json['definitions']); + $this->assertArrayHasKey('Robin.jsonld', $json['definitions']); } public function testArraySchemaWithMultipleUnionTypesJsonApi(): void @@ -183,7 +185,7 @@ public function testArraySchemaWithTypeFactory(): void $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); - $this->assertEquals($json['definitions']['Foo.jsonld.output']['properties']['expiration'], ['type' => 'string', 'format' => 'date']); + $this->assertEquals($json['definitions']['Foo.jsonld']['properties']['expiration'], ['type' => 'string', 'format' => 'date']); } /** @@ -195,7 +197,7 @@ public function testWritableNonResourceRef(): void $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); - $this->assertEquals($json['definitions']['SaveProduct.jsonld.input']['properties']['codes']['items']['$ref'], '#/definitions/ProductCode.jsonld.input'); + $this->assertEquals($json['definitions']['SaveProduct.jsonld']['properties']['codes']['items']['$ref'], '#/definitions/ProductCode.jsonld'); } /** @@ -207,8 +209,8 @@ public function testOpenApiResourceRefIsNotOverwritten(): void $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); - $this->assertEquals('#/definitions/DummyFriend', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld.output']['properties']['itemDto']['$ref']); - $this->assertEquals('#/definitions/DummyDate', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld.output']['properties']['collectionDto']['items']['$ref']); + $this->assertEquals('#/definitions/DummyFriend', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld']['properties']['itemDto']['$ref']); + $this->assertEquals('#/definitions/DummyDate', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld']['properties']['collectionDto']['items']['$ref']); } /** @@ -220,7 +222,7 @@ public function testSubSchemaJsonLd(): void $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); - $this->assertArrayHasKey('@id', $json['definitions']['ThirdLevel.jsonld-friends.output']['properties']); + $this->assertArrayHasKey('@id', $json['definitions']['ThirdLevel.jsonld-friends']['properties']); } public function testJsonApiIncludesSchema(): void @@ -332,4 +334,17 @@ public function testResourceWithEnumPropertiesSchema(): void $properties['genders'] ); } + + /** + * @group legacy + * TODO: find a way to keep required properties if needed + */ + public function testPatchSchemaRequiredProperties(): void + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\PatchRequired\PatchMe', '--format' => 'json']); + $result = $this->tester->getDisplay(); + $json = json_decode($result, associative: true); + + $this->assertEquals(['b'], $json['definitions']['PatchMe']['required']); + } }