Skip to content

Commit 02f1023

Browse files
committed
feat(jsonschema): make all required properties optional in PATCH operation with 'json' format
by @ttskch see api-platform#6394
1 parent 1033064 commit 02f1023

File tree

3 files changed

+79
-20
lines changed

3 files changed

+79
-20
lines changed

src/JsonSchema/SchemaFactory.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ public function buildSchema(string $className, string $format = 'json', string $
8888
return $schema;
8989
}
9090

91+
$isJsonMergePatch = 'json' === $format && 'PATCH' === $operation->getMethod() && Schema::TYPE_INPUT === $type;
92+
$skipRequiredProperties = false;
93+
94+
if ($isJsonMergePatch) {
95+
if (null === ($skipRequiredProperties = $operation->getExtraProperties()['patch_skip_schema_required_properties'] ?? null)) {
96+
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.");
97+
}
98+
99+
if (true === $skipRequiredProperties) {
100+
$definitionName .= self::PATCH_SCHEMA_POSTFIX;
101+
}
102+
}
103+
91104
if (!isset($schema['$ref']) && !isset($schema['type'])) {
92105
$ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
93106
if ($forceCollection || ('POST' !== $method && $operation instanceof CollectionOperationInterface)) {
@@ -129,14 +142,15 @@ public function buildSchema(string $className, string $format = 'json', string $
129142
}
130143

131144
$options = ['schema_type' => $type] + $this->getFactoryOptions($serializerContext, $validationGroups, $operation instanceof HttpOperation ? $operation : null);
145+
132146
foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
133147
$propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options);
134148
if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) {
135149
continue;
136150
}
137151

138152
$normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName;
139-
if ($propertyMetadata->isRequired()) {
153+
if ($propertyMetadata->isRequired() && !$skipRequiredProperties) {
140154
$definition['required'][] = $normalizedPropertyName;
141155
}
142156

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\PatchRequired;
15+
16+
use ApiPlatform\Metadata\Patch;
17+
use Symfony\Component\Validator\Constraints\NotNull;
18+
19+
#[Patch(uriTemplate: '/patch_required_stuff', provider: [self::class, 'provide'])]
20+
final class PatchMe
21+
{
22+
public ?string $a = null;
23+
#[NotNull]
24+
public ?string $b = null;
25+
26+
public static function provide(): self
27+
{
28+
return new self();
29+
}
30+
}

tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php

+34-19
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DocumentDummy;
1717
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
18+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
1819
use Symfony\Bundle\FrameworkBundle\Console\Application;
1920
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
2021
use Symfony\Component\Console\Tester\ApplicationTester;
@@ -24,6 +25,7 @@
2425
*/
2526
class JsonSchemaGenerateCommandTest extends KernelTestCase
2627
{
28+
use ExpectDeprecationTrait;
2729
private ApplicationTester $tester;
2830

2931
private string $entityClass;
@@ -76,9 +78,9 @@ public function testExecuteWithJsonldTypeInput(): void
7678
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_post', '--format' => 'jsonld', '--type' => 'input']);
7779
$result = $this->tester->getDisplay();
7880

79-
$this->assertStringContainsString('@id', $result);
80-
$this->assertStringContainsString('@context', $result);
81-
$this->assertStringContainsString('@type', $result);
81+
$this->assertStringNotContainsString('@id', $result);
82+
$this->assertStringNotContainsString('@context', $result);
83+
$this->assertStringNotContainsString('@type', $result);
8284
}
8385

8486
/**
@@ -103,24 +105,24 @@ public function testArraySchemaWithReference(): void
103105
$result = $this->tester->getDisplay();
104106
$json = json_decode($result, associative: true);
105107

106-
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['tests'], [
108+
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['tests'], [
107109
'type' => 'string',
108110
'foo' => 'bar',
109111
]);
110112

111-
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['nonResourceTests'], [
113+
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['nonResourceTests'], [
112114
'type' => 'array',
113115
'items' => [
114-
'$ref' => '#/definitions/NonResourceTestEntity.jsonld-write.input',
116+
'$ref' => '#/definitions/NonResourceTestEntity.jsonld-write',
115117
],
116118
]);
117119

118-
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['description'], [
120+
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['description'], [
119121
'maxLength' => 255,
120122
]);
121123

122-
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write.input']['properties']['type'], [
123-
'$ref' => '#/definitions/TestEntity.jsonld-write.input',
124+
$this->assertEquals($json['definitions']['BagOfTests.jsonld-write']['properties']['type'], [
125+
'$ref' => '#/definitions/TestEntity.jsonld-write',
124126
]);
125127
}
126128

@@ -130,14 +132,14 @@ public function testArraySchemaWithMultipleUnionTypesJsonLd(): void
130132
$result = $this->tester->getDisplay();
131133
$json = json_decode($result, associative: true);
132134

133-
$this->assertEquals($json['definitions']['Nest.jsonld.output']['properties']['owner']['anyOf'], [
134-
['$ref' => '#/definitions/Wren.jsonld.output'],
135-
['$ref' => '#/definitions/Robin.jsonld.output'],
135+
$this->assertEquals($json['definitions']['Nest.jsonld']['properties']['owner']['anyOf'], [
136+
['$ref' => '#/definitions/Wren.jsonld'],
137+
['$ref' => '#/definitions/Robin.jsonld'],
136138
['type' => 'null'],
137139
]);
138140

139-
$this->assertArrayHasKey('Wren.jsonld.output', $json['definitions']);
140-
$this->assertArrayHasKey('Robin.jsonld.output', $json['definitions']);
141+
$this->assertArrayHasKey('Wren.jsonld', $json['definitions']);
142+
$this->assertArrayHasKey('Robin.jsonld', $json['definitions']);
141143
}
142144

143145
public function testArraySchemaWithMultipleUnionTypesJsonApi(): void
@@ -183,7 +185,7 @@ public function testArraySchemaWithTypeFactory(): void
183185
$result = $this->tester->getDisplay();
184186
$json = json_decode($result, associative: true);
185187

186-
$this->assertEquals($json['definitions']['Foo.jsonld.output']['properties']['expiration'], ['type' => 'string', 'format' => 'date']);
188+
$this->assertEquals($json['definitions']['Foo.jsonld']['properties']['expiration'], ['type' => 'string', 'format' => 'date']);
187189
}
188190

189191
/**
@@ -195,7 +197,7 @@ public function testWritableNonResourceRef(): void
195197
$result = $this->tester->getDisplay();
196198
$json = json_decode($result, associative: true);
197199

198-
$this->assertEquals($json['definitions']['SaveProduct.jsonld.input']['properties']['codes']['items']['$ref'], '#/definitions/ProductCode.jsonld.input');
200+
$this->assertEquals($json['definitions']['SaveProduct.jsonld']['properties']['codes']['items']['$ref'], '#/definitions/ProductCode.jsonld');
199201
}
200202

201203
/**
@@ -207,8 +209,8 @@ public function testOpenApiResourceRefIsNotOverwritten(): void
207209
$result = $this->tester->getDisplay();
208210
$json = json_decode($result, associative: true);
209211

210-
$this->assertEquals('#/definitions/DummyFriend', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld.output']['properties']['itemDto']['$ref']);
211-
$this->assertEquals('#/definitions/DummyDate', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld.output']['properties']['collectionDto']['items']['$ref']);
212+
$this->assertEquals('#/definitions/DummyFriend', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld']['properties']['itemDto']['$ref']);
213+
$this->assertEquals('#/definitions/DummyDate', $json['definitions']['Issue6299.Issue6299OutputDto.jsonld']['properties']['collectionDto']['items']['$ref']);
212214
}
213215

214216
/**
@@ -220,7 +222,7 @@ public function testSubSchemaJsonLd(): void
220222
$result = $this->tester->getDisplay();
221223
$json = json_decode($result, associative: true);
222224

223-
$this->assertArrayHasKey('@id', $json['definitions']['ThirdLevel.jsonld-friends.output']['properties']);
225+
$this->assertArrayHasKey('@id', $json['definitions']['ThirdLevel.jsonld-friends']['properties']);
224226
}
225227

226228
public function testJsonApiIncludesSchema(): void
@@ -332,4 +334,17 @@ public function testResourceWithEnumPropertiesSchema(): void
332334
$properties['genders']
333335
);
334336
}
337+
338+
/**
339+
* @group legacy
340+
* TODO: find a way to keep required properties if needed
341+
*/
342+
public function testPatchSchemaRequiredProperties(): void
343+
{
344+
$this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\PatchRequired\PatchMe', '--format' => 'json']);
345+
$result = $this->tester->getDisplay();
346+
$json = json_decode($result, associative: true);
347+
348+
$this->assertEquals(['b'], $json['definitions']['PatchMe']['required']);
349+
}
335350
}

0 commit comments

Comments
 (0)