Skip to content

Commit 0f39cb3

Browse files
committed
fix(hydra): keep jsonapi input schemas out of the json rewrite
The Hydra schema factory rewrote every input schema to json. Because it wraps the JSON:API factory, a jsonapi request body was documented as a plain object without the data/attributes/relationships envelope whenever jsonld was enabled. Only jsonapi is now excluded from the rewrite, so other formats are unchanged. JSON:API input definitions get an .input postfix, e.g. Dummy.jsonapi.input, so they no longer share a name with the output definition, which requires data.id and lists included. The input definition requires only type on POST and drops included.
1 parent c810012 commit 0f39cb3

3 files changed

Lines changed: 52 additions & 3 deletions

File tree

‎src/Hydra/JsonSchema/SchemaFactory.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public function buildSchema(string $className, string $format = 'jsonld', string
109109
// Strictly speaking, it is possible to include `@id` or `@context` in the input,
110110
// but the generated JSON Schema does not include `"additionalProperties": false` by default,
111111
// so it is possible to include `@id` or `@context` in the input even if the input schema is a JSON schema.
112-
if (Schema::TYPE_INPUT === $type) {
112+
if (Schema::TYPE_INPUT === $type && 'jsonapi' !== $format) {
113113
$format = 'json';
114114
}
115115

‎src/JsonApi/JsonSchema/SchemaFactory.php‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
4646
*/
4747
public const DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS = 'disable_json_schema_serializer_groups';
4848

49+
private const INPUT_SCHEMA_POSTFIX = 'input';
4950
private const COLLECTION_BASE_SCHEMA_NAME = 'JsonApiCollectionBaseSchema';
5051
private const COLLECTION_BASE_SCHEMA_NAME_NO_PAGINATION = 'JsonApiCollectionBaseSchemaNoPagination';
5152

@@ -127,7 +128,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
127128
}
128129

129130
$schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection);
130-
$definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $jsonApiSerializerContext + ['schema_type' => $type]);
131+
$definitionName = $this->createDefinitionName($className, $format, $inputOrOutputClass, $operation, $jsonApiSerializerContext, $type);
131132
$prefix = $this->getSchemaUriPrefix($schema->getVersion());
132133
$definitions = $schema->getDefinitions();
133134
$collectionKey = $schema->getItemsDefinitionKey();
@@ -296,7 +297,7 @@ private function buildDefinitionPropertiesSchema(string $key, string $documentDe
296297
if ($relation = $this->getRelationship($propertyClass, $propertyName, $serializerContext)) {
297298
[$isOne, $relatedClasses] = $relation;
298299
$refs = [];
299-
foreach ($relatedClasses as $relatedClassName) {
300+
foreach (Schema::TYPE_OUTPUT === $type ? $relatedClasses : [] as $relatedClassName) {
300301
$operation = $this->findOperation($relatedClassName, $type, null, $serializerContext);
301302
$inputOrOutputClass = $this->findOutputClass($relatedClassName, $type, $operation, $serializerContext);
302303
$serializerContext ??= $this->getSerializerContext($operation, $type);
@@ -355,6 +356,9 @@ private function buildDefinitionPropertiesSchema(string $key, string $documentDe
355356
'type' => 'object',
356357
'properties' => $relationships,
357358
];
359+
}
360+
361+
if (Schema::TYPE_OUTPUT === $type && \count($relationships) > 0) {
358362
$included = [
359363
'included' => [
360364
'description' => 'Related resources requested via the "include" query parameter.',
@@ -386,6 +390,13 @@ private function buildDefinitionPropertiesSchema(string $key, string $documentDe
386390
] + $included;
387391
}
388392

393+
private function createDefinitionName(string $className, string $format, ?string $inputOrOutputClass, ?Operation $operation, ?array $serializerContext, string $type): string
394+
{
395+
$name = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, ($serializerContext ?? []) + ['schema_type' => $type]);
396+
397+
return Schema::TYPE_INPUT === $type ? $name.'.'.self::INPUT_SCHEMA_POSTFIX : $name;
398+
}
399+
389400
private function getRelationship(string $resourceClass, string $property, ?array $serializerContext): ?array
390401
{
391402
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $serializerContext ?? []);

‎tests/Functional/OpenApiTest.php‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,44 @@ public function testOpenApiReferenceInResponsesUsesDollarRef(): void
138138
$this->assertArrayNotHasKey('ref', $responses['401']);
139139
}
140140

141+
public function testNonJsonLdRequestBodiesKeepTheirFormatDefinition(): void
142+
{
143+
$json = self::createClient()->request('GET', '/docs', [
144+
'headers' => ['Accept' => 'application/vnd.openapi+json'],
145+
])->toArray();
146+
147+
$content = $json['paths']['/dummies']['post']['requestBody']['content'];
148+
149+
$this->assertSame('#/components/schemas/Dummy', $content['application/xml']['schema']['$ref']);
150+
$this->assertSame('#/components/schemas/Dummy', $content['application/ld+json']['schema']['$ref']);
151+
}
152+
153+
public function testJsonApiRequestBodyUsesItsOwnInputDefinition(): void
154+
{
155+
$json = self::createClient()->request('GET', '/docs', [
156+
'headers' => ['Accept' => 'application/vnd.openapi+json'],
157+
])->toArray();
158+
$schemas = $json['components']['schemas'];
159+
160+
$inputName = $this->definitionName($json['paths']['/dummies']['post']['requestBody']['content']['application/vnd.api+json']['schema']['$ref']);
161+
$outputName = $this->definitionName($json['paths']['/dummies/{id}']['get']['responses']['200']['content']['application/vnd.api+json']['schema']['$ref']);
162+
163+
$this->assertSame('Dummy.jsonapi', $outputName);
164+
$this->assertNotSame($outputName, $inputName);
165+
166+
$input = $schemas[$inputName];
167+
$this->assertSame(['type'], $input['properties']['data']['required']);
168+
$this->assertArrayHasKey('attributes', $input['properties']['data']['properties']);
169+
$this->assertArrayNotHasKey('included', $input['properties']);
170+
171+
$this->assertSame(['type', 'id'], $schemas[$outputName]['properties']['data']['required']);
172+
}
173+
174+
private function definitionName(string $ref): string
175+
{
176+
return substr($ref, \strlen('#/components/schemas/'));
177+
}
178+
141179
public function testDeprecatedPutDoesNotLeakIntoNestedResourceSchema(): void
142180
{
143181
$response = self::createClient()->request('GET', '/docs', [

0 commit comments

Comments
 (0)