From 8031daed979389baf0794ef3b22d26b188ddd106 Mon Sep 17 00:00:00 2001 From: Gwendolen Lynch Date: Tue, 12 Mar 2024 13:19:59 +0100 Subject: [PATCH] fix(jsonschema): don't skip remaining multiple union types --- src/JsonSchema/SchemaFactory.php | 25 +++++++--- .../TestBundle/ApiResource/Issue6212/Bird.php | 21 ++++++++ .../ApiResource/Issue6212/Robin.php | 35 +++++++++++++ .../TestBundle/ApiResource/Issue6212/Wren.php | 36 +++++++++++++ .../TestBundle/Entity/Issue6212/Nest.php | 50 +++++++++++++++++++ .../Command/JsonSchemaGenerateCommandTest.php | 16 ++++++ 6 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 tests/Fixtures/TestBundle/ApiResource/Issue6212/Bird.php create mode 100644 tests/Fixtures/TestBundle/ApiResource/Issue6212/Robin.php create mode 100644 tests/Fixtures/TestBundle/ApiResource/Issue6212/Wren.php create mode 100644 tests/Fixtures/TestBundle/Entity/Issue6212/Nest.php diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index a128a89687..57f9552d20 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -196,10 +196,13 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str // property schema is created in SchemaPropertyMetadataFactory, but it cannot build resource reference ($ref) // complete property schema with resource reference ($ref) only if it's related to an object $version = $schema->getVersion(); - $subSchema = new Schema($version); - $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema + $refs = []; + $isNullable = null; foreach ($types as $type) { + $subSchema = new Schema($version); + $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema + // TODO: in 3.3 add trigger_deprecation() as type factories are not used anymore, we moved this logic to SchemaPropertyMetadataFactory so that it gets cached if ($typeFromFactory = $this->typeFactory?->getType($type, 'jsonschema', $propertyMetadata->isReadableLink(), $serializerContext)) { $propertySchema = $typeFromFactory; @@ -230,14 +233,20 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str break; } - if ($type->isNullable()) { - $propertySchema['anyOf'] = [['$ref' => $subSchema['$ref']], ['type' => 'null']]; - } else { - $propertySchema['$ref'] = $subSchema['$ref']; - } + $refs[] = ['$ref' => $subSchema['$ref']]; + $isNullable = $isNullable ?? $type->isNullable(); + } + if ($isNullable) { + $refs[] = ['type' => 'null']; + } + + if (\count($refs) > 1) { + $propertySchema['anyOf'] = $refs; + unset($propertySchema['type']); + } elseif (1 === \count($refs)) { + $propertySchema['$ref'] = $refs[0]['$ref']; unset($propertySchema['type']); - break; } $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema); diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6212/Bird.php b/tests/Fixtures/TestBundle/ApiResource/Issue6212/Bird.php new file mode 100644 index 0000000000..9aeb2c5ae7 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6212/Bird.php @@ -0,0 +1,21 @@ + + * + * 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\Issue6212; + +interface Bird extends \JsonSerializable +{ + public function getName(): ?string; + + public function getAge(): ?int; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6212/Robin.php b/tests/Fixtures/TestBundle/ApiResource/Issue6212/Robin.php new file mode 100644 index 0000000000..d3209a8e3c --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6212/Robin.php @@ -0,0 +1,35 @@ + + * + * 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\Issue6212; + +final class Robin implements Bird +{ + public ?string $name = null; + public ?int $age = null; + + public function getName(): ?string + { + return $this->name; + } + + public function getAge(): ?int + { + return $this->age; + } + + public function jsonSerialize(): array + { + return get_object_vars($this); + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue6212/Wren.php b/tests/Fixtures/TestBundle/ApiResource/Issue6212/Wren.php new file mode 100644 index 0000000000..c9f09e56ba --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue6212/Wren.php @@ -0,0 +1,36 @@ + + * + * 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\Issue6212; + +final class Wren implements Bird +{ + public ?string $name = null; + public ?int $age = null; + public ?int $weight = null; + + public function getName(): ?string + { + return $this->name; + } + + public function getAge(): ?int + { + return $this->age; + } + + public function jsonSerialize(): array + { + return get_object_vars($this); + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue6212/Nest.php b/tests/Fixtures/TestBundle/Entity/Issue6212/Nest.php new file mode 100644 index 0000000000..fe38ba2499 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue6212/Nest.php @@ -0,0 +1,50 @@ + + * + * 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\Entity\Issue6212; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6212\Bird; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6212\Robin; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6212\Wren; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource] +#[ORM\Entity] +class Nest +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(type: 'json')] + private ?Bird $owner; + + public function getId(): ?int + { + return $this->id; + } + + public function getOwner(): ?Bird + { + return $this->owner; + } + + public function setOwner(Wren|Robin|null $owner): static + { + $this->owner = $owner; + + return $this; + } +} diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index 8800fd5265..3e4ed9d484 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -124,6 +124,22 @@ public function testArraySchemaWithReference(): void ]); } + public function testArraySchemaWithMultipleUnionTypes(): void + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6212\Nest', '--type' => 'output']); + $result = $this->tester->getDisplay(); + $json = json_decode($result, associative: true); + + $this->assertEquals($json['definitions']['Nest.jsonld']['properties']['owner']['anyOf'], [ + ['$ref' => '#/definitions/Wren.jsonld'], + ['$ref' => '#/definitions/Robin.jsonld'], + ['type' => 'null'], + ]); + + $this->assertArrayHasKey('Wren.jsonld', $json['definitions']); + $this->assertArrayHasKey('Robin.jsonld', $json['definitions']); + } + /** * TODO: add deprecation (TypeFactory will be deprecated in api platform 3.3). *