Skip to content

Properties with multiple type unions generate incorrect OpenAPI schema #6212

Open
@GwendolenLynch

Description

@GwendolenLynch

API Platform version(s) affected: 3.2

Description
Currently (3.2.16) with the model below the owner property will generate the schema:

"owner": {
    "anyOf": [
        {
            "$ref": "#/components/schemas/Wren"
        },
        {
            "type": "null"
        }
    ]
}

Instead of:

"owner": {
    "anyOf": [
        {
            "$ref": "#/components/schemas/Wren"
        },
        {
            "$ref": "#/components/schemas/Robin"
        },
        {
            "type": "null"
        }
    ]
}

How to reproduce

#[ApiResource]
#[ORM\Entity]
class Nest
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(type: 'bird')]
    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;
    }
}
interface Bird
{
    public function getName(): ?string;

    public function getAge(): ?int;
}
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;
    }
}
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;
    }
}

Possible Solution
I dug in a bit and it stems from handling in SchemaFactory::buildPropertySchema. Simply removing the final break here make this particular problem disappear but breaks tests. So I hacked up a more robust proof-of-concept fix/patch below that addresses this issue (and passes existing tests).

diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php
index a128a8968..9a21d47d3 100644
--- a/src/JsonSchema/SchemaFactory.php
+++ b/src/JsonSchema/SchemaFactory.php
@@ -196,10 +196,13 @@
         // 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
 
-        foreach ($types as $type) {
             // 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,25 @@
                 break;
             }
 
-            if ($type->isNullable()) {
-                $propertySchema['anyOf'] = [['$ref' => $subSchema['$ref']], ['type' => 'null']];
-            } else {
-                $propertySchema['$ref'] = $subSchema['$ref'];
+            $isNullable = $isNullable ?? $type->isNullable();
+            $refs[$subSchema['$ref']] = '$ref';
+        }
+
+        if (\count($refs) > 1) {
+            $anyOf = [];
+            foreach (array_keys($refs) as $ref) {
+                $anyOf[] = ['$ref' => $ref];
+            }
+            $propertySchema['anyOf'] = $anyOf;
+
+            if ($isNullable) {
+                $propertySchema['anyOf'][] = ['type' => 'null'];
             }
 
             unset($propertySchema['type']);
-            break;
+        } elseif (\count($refs) === 1) {
+            $propertySchema['$ref'] = array_keys($refs)[0];
+            unset($propertySchema['type']);
         }
 
         $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions