Skip to content

Commit 8999b60

Browse files
authored
fix(jsonapi): correct relationship schemas in generated json schema (#8321)
1 parent 84e7818 commit 8999b60

3 files changed

Lines changed: 147 additions & 1 deletion

File tree

src/JsonApi/JsonSchema/SchemaFactory.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
6363
'format' => 'iri-reference',
6464
],
6565
],
66+
'required' => ['type', 'id'],
6667
];
6768
private const PROPERTY_PROPS = [
6869
'id' => [
@@ -321,7 +322,10 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
321322

322323
$refs[$this->getSchemaUriPrefix($schema->getVersion()).$definitionName] = '$ref';
323324
}
324-
$relatedDefinitions[$propertyName] = array_flip($refs);
325+
// keep one entry per related definition: a polymorphic relation targets several resource classes, all of which may appear in "included"
326+
foreach (array_keys($refs) as $ref) {
327+
$relatedDefinitions[$ref] = ['$ref' => $ref];
328+
}
325329
if ($isOne) {
326330
$relationships[$propertyName]['properties']['data'] = [
327331
'oneOf' => [
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\JsonApi\Tests\Fixtures;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
18+
/**
19+
* A second related resource used to exercise polymorphic (union-typed) relationships.
20+
*/
21+
#[ApiResource]
22+
class OtherRelatedDummy
23+
{
24+
private ?int $id = null;
25+
26+
public ?string $label = null;
27+
28+
public function getId(): ?int
29+
{
30+
return $this->id;
31+
}
32+
33+
public function setId(?int $id): void
34+
{
35+
$this->id = $id;
36+
}
37+
}

src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\JsonApi\JsonSchema\SchemaFactory;
1717
use ApiPlatform\JsonApi\Tests\Fixtures\Dummy;
18+
use ApiPlatform\JsonApi\Tests\Fixtures\OtherRelatedDummy;
1819
use ApiPlatform\JsonApi\Tests\Fixtures\RelatedDummy;
1920
use ApiPlatform\JsonSchema\DefinitionNameFactory;
2021
use ApiPlatform\JsonSchema\Schema;
@@ -230,6 +231,110 @@ public function testRelationIsExcludedFromAttributes(): void
230231
$this->assertArrayHasKey('relatedDummy', $dataProperties['relationships']['properties']);
231232
}
232233

234+
public function testRelationshipLinkageRequiresTypeAndId(): void
235+
{
236+
$schemaFactory = $this->buildSchemaFactoryWithRelation();
237+
$resultSchema = $schemaFactory->buildSchema(Dummy::class, 'jsonapi');
238+
239+
$definitions = $resultSchema->getDefinitions();
240+
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
241+
$dataProperties = $definitions[$rootDefinitionKey]['properties']['data']['properties'];
242+
243+
// a resource identifier object MUST contain type and id, @see https://jsonapi.org/format/#document-resource-identifier-objects
244+
$linkage = $dataProperties['relationships']['properties']['relatedDummy']['properties']['data']['oneOf'][1];
245+
$this->assertSame('object', $linkage['type']);
246+
$this->assertSame(['type', 'id'], $linkage['required']);
247+
}
248+
249+
public function testIncludedListsAllPolymorphicRelationTargets(): void
250+
{
251+
$schemaFactory = $this->buildSchemaFactoryWithPolymorphicRelation();
252+
$resultSchema = $schemaFactory->buildSchema(Dummy::class, 'jsonapi');
253+
254+
$definitions = $resultSchema->getDefinitions();
255+
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
256+
$included = $definitions[$rootDefinitionKey]['properties']['included'];
257+
258+
$refs = array_column($included['items']['anyOf'], '$ref');
259+
$this->assertContains('#/definitions/RelatedDummy.jsonapi', $refs);
260+
$this->assertContains('#/definitions/OtherRelatedDummy.jsonapi', $refs, 'every target of a polymorphic relation must be listed in included');
261+
}
262+
263+
private function buildSchemaFactoryWithPolymorphicRelation(): SchemaFactory
264+
{
265+
$dummyOperation = (new Get())->withName('get')->withShortName('Dummy');
266+
$relatedOperation = (new Get())->withName('get')->withShortName('RelatedDummy');
267+
$otherRelatedOperation = (new Get())->withName('get')->withShortName('OtherRelatedDummy');
268+
269+
$resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
270+
$resourceMetadataFactory->create(Dummy::class)->willReturn(
271+
new ResourceMetadataCollection(Dummy::class, [
272+
(new ApiResource())->withOperations(new Operations(['get' => $dummyOperation])),
273+
])
274+
);
275+
$resourceMetadataFactory->create(RelatedDummy::class)->willReturn(
276+
new ResourceMetadataCollection(RelatedDummy::class, [
277+
(new ApiResource())->withOperations(new Operations(['get' => $relatedOperation])),
278+
])
279+
);
280+
$resourceMetadataFactory->create(OtherRelatedDummy::class)->willReturn(
281+
new ResourceMetadataCollection(OtherRelatedDummy::class, [
282+
(new ApiResource())->withOperations(new Operations(['get' => $otherRelatedOperation])),
283+
])
284+
);
285+
286+
$propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
287+
$propertyNameCollectionFactory->create(Dummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['id', 'name', 'relatedDummy']));
288+
$propertyNameCollectionFactory->create(RelatedDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['id', 'name']));
289+
$propertyNameCollectionFactory->create(OtherRelatedDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['id', 'label']));
290+
291+
$propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class);
292+
$propertyMetadataFactory->create(Dummy::class, 'id', Argument::type('array'))->willReturn(
293+
(new ApiProperty())->withNativeType(Type::int())->withReadable(true)->withSchema(['type' => 'integer'])
294+
);
295+
$propertyMetadataFactory->create(Dummy::class, 'name', Argument::type('array'))->willReturn(
296+
(new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withSchema(['type' => 'string'])
297+
);
298+
$propertyMetadataFactory->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn(
299+
(new ApiProperty())->withNativeType(Type::union(Type::object(RelatedDummy::class), Type::object(OtherRelatedDummy::class)))->withReadable(true)->withSchema(['type' => Schema::UNKNOWN_TYPE])
300+
);
301+
$propertyMetadataFactory->create(RelatedDummy::class, 'id', Argument::type('array'))->willReturn(
302+
(new ApiProperty())->withNativeType(Type::int())->withReadable(true)->withSchema(['type' => 'integer'])
303+
);
304+
$propertyMetadataFactory->create(RelatedDummy::class, 'name', Argument::type('array'))->willReturn(
305+
(new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withSchema(['type' => 'string'])
306+
);
307+
$propertyMetadataFactory->create(OtherRelatedDummy::class, 'id', Argument::type('array'))->willReturn(
308+
(new ApiProperty())->withNativeType(Type::int())->withReadable(true)->withSchema(['type' => 'integer'])
309+
);
310+
$propertyMetadataFactory->create(OtherRelatedDummy::class, 'label', Argument::type('array'))->willReturn(
311+
(new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withSchema(['type' => 'string'])
312+
);
313+
314+
$resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class);
315+
$resourceClassResolver->isResourceClass(Dummy::class)->willReturn(true);
316+
$resourceClassResolver->isResourceClass(RelatedDummy::class)->willReturn(true);
317+
$resourceClassResolver->isResourceClass(OtherRelatedDummy::class)->willReturn(true);
318+
319+
$definitionNameFactory = new DefinitionNameFactory(null);
320+
321+
$baseSchemaFactory = new BaseSchemaFactory(
322+
resourceMetadataFactory: $resourceMetadataFactory->reveal(),
323+
propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(),
324+
propertyMetadataFactory: $propertyMetadataFactory->reveal(),
325+
resourceClassResolver: $resourceClassResolver->reveal(),
326+
definitionNameFactory: $definitionNameFactory,
327+
);
328+
329+
return new SchemaFactory(
330+
schemaFactory: $baseSchemaFactory,
331+
propertyMetadataFactory: $propertyMetadataFactory->reveal(),
332+
resourceClassResolver: $resourceClassResolver->reveal(),
333+
resourceMetadataFactory: $resourceMetadataFactory->reveal(),
334+
definitionNameFactory: $definitionNameFactory,
335+
);
336+
}
337+
233338
private function buildSchemaFactoryWithRelation(): SchemaFactory
234339
{
235340
$dummyOperation = (new Get())->withName('get')->withShortName('Dummy');

0 commit comments

Comments
 (0)