Skip to content

Commit efb4a00

Browse files
complete property schema on SchemaFactory
1 parent 4689004 commit efb4a00

File tree

16 files changed

+293
-185
lines changed

16 files changed

+293
-185
lines changed

features/openapi/docs.feature

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,12 @@ Feature: Documentation support
8686
{
8787
"default": "male",
8888
"example": "male",
89-
"type": "string",
89+
"type": ["string", "null"],
9090
"enum": [
9191
"male",
9292
"female",
9393
null
94-
],
95-
"nullable": true
94+
]
9695
}
9796
"""
9897
And the "playMode" property exists for the OpenAPI class "VideoGame"
@@ -238,8 +237,7 @@ Feature: Documentation support
238237
"type": "string"
239238
},
240239
"property": {
241-
"type": "string",
242-
"nullable": true
240+
"type": ["string", "null"]
243241
},
244242
"required": {
245243
"type": "boolean"
@@ -314,8 +312,10 @@ Feature: Documentation support
314312
"anyOf":[
315313
{
316314
"$ref":"#/components/schemas/ResourceRelated"
315+
},
316+
{
317+
"type":"null"
317318
}
318-
],
319-
"nullable":true
319+
]
320320
}
321321
"""

src/Hydra/JsonSchema/SchemaFactory.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,15 @@ public function buildSchema(string $className, string $format = 'jsonld', string
9191
$items = $schema['items'];
9292
unset($schema['items']);
9393

94-
$nullableStringDefinition = ['type' => 'string'];
95-
9694
switch ($schema->getVersion()) {
95+
// JSON Schema + OpenAPI 3.1
96+
case Schema::VERSION_OPENAPI:
9797
case Schema::VERSION_JSON_SCHEMA:
9898
$nullableStringDefinition = ['type' => ['string', 'null']];
9999
break;
100-
case Schema::VERSION_OPENAPI:
101-
$nullableStringDefinition = ['type' => 'string', 'nullable' => true];
100+
// Swagger
101+
default:
102+
$nullableStringDefinition = ['type' => 'string'];
102103
break;
103104
}
104105

src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
use ApiPlatform\Exception\PropertyNotFoundException;
1717
use ApiPlatform\Metadata\ApiProperty;
1818
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
19-
use ApiPlatform\Util\ResourceClassInfoTrait;
19+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
20+
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
2021
use Ramsey\Uuid\UuidInterface;
2122
use Symfony\Component\PropertyInfo\Type;
2223
use Symfony\Component\Uid\Ulid;
@@ -29,8 +30,9 @@ final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInte
2930
{
3031
use ResourceClassInfoTrait;
3132

32-
public function __construct(private readonly ?PropertyMetadataFactoryInterface $decorated = null)
33+
public function __construct(ResourceClassResolverInterface $resourceClassResolver, private readonly ?PropertyMetadataFactoryInterface $decorated = null)
3334
{
35+
$this->resourceClassResolver = $resourceClassResolver;
3436
}
3537

3638
public function create(string $resourceClass, string $property, array $options = []): ApiProperty
@@ -47,43 +49,43 @@ public function create(string $resourceClass, string $property, array $options =
4749

4850
$propertySchema = $propertyMetadata->getSchema() ?? [];
4951

50-
if (!array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) {
52+
if (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) {
5153
$propertySchema['readOnly'] = true;
5254
}
5355

54-
if (!array_key_exists('writeOnly', $propertySchema) && false === $propertyMetadata->isReadable()) {
56+
if (!\array_key_exists('writeOnly', $propertySchema) && false === $propertyMetadata->isReadable()) {
5557
$propertySchema['writeOnly'] = true;
5658
}
5759

58-
if (!array_key_exists('description', $propertySchema) && null !== ($description = $propertyMetadata->getDescription())) {
60+
if (!\array_key_exists('description', $propertySchema) && null !== ($description = $propertyMetadata->getDescription())) {
5961
$propertySchema['description'] = $description;
6062
}
6163

6264
// see https://github.com/json-schema-org/json-schema-spec/pull/737
63-
if (!array_key_exists('deprecated', $propertySchema) && null !== $propertyMetadata->getDeprecationReason()) {
65+
if (!\array_key_exists('deprecated', $propertySchema) && null !== $propertyMetadata->getDeprecationReason()) {
6466
$propertySchema['deprecated'] = true;
6567
}
6668

6769
// externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
6870
// See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
69-
if (!array_key_exists('externalDocs', $propertySchema) && null !== ($iri = $propertyMetadata->getTypes()[0] ?? null)) {
71+
if (!\array_key_exists('externalDocs', $propertySchema) && null !== ($iri = $propertyMetadata->getTypes()[0] ?? null)) {
7072
$propertySchema['externalDocs'] = ['url' => $iri];
7173
}
7274

7375
$types = $propertyMetadata->getBuiltinTypes() ?? [];
7476

75-
if (!array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!\count($types) || null === ($className = $types[0]->getClassName()) || !$this->isResourceClass($className))) {
77+
if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!\count($types) || null === ($className = $types[0]->getClassName()) || !$this->isResourceClass($className))) {
7678
if ($default instanceof \BackedEnum) {
7779
$default = $default->value;
7880
}
7981
$propertySchema['default'] = $default;
8082
}
8183

82-
if (!array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
84+
if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) {
8385
$propertySchema['example'] = $example;
8486
}
8587

86-
if (!array_key_exists('example', $propertySchema) && array_key_exists('default', $propertySchema)) {
88+
if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) {
8789
$propertySchema['example'] = $propertySchema['default'];
8890
}
8991

@@ -115,7 +117,7 @@ public function create(string $resourceClass, string $property, array $options =
115117
}
116118

117119
$propertyType = $this->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $propertyMetadata->isReadableLink());
118-
if (!\in_array($propertyType, $valueSchema)) {
120+
if (!\in_array($propertyType, $valueSchema, true)) {
119121
$valueSchema[] = $propertyType;
120122
}
121123
}
@@ -217,7 +219,7 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl
217219
];
218220
}
219221

220-
if (is_a($className, \BackedEnum::class, true)) {
222+
if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) {
221223
$enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases());
222224

223225
$type = \is_string($enumCases[0] ?? '') ? 'string' : 'int';
@@ -232,6 +234,13 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl
232234
];
233235
}
234236

237+
if (true !== $readableLink && $this->isResourceClass($className)) {
238+
return [
239+
'type' => 'string',
240+
'format' => 'iri-reference',
241+
];
242+
}
243+
235244
return ['type' => 'string'];
236245
}
237246

src/JsonSchema/SchemaFactory.php

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,6 @@ public function buildSchema(string $className, string $format = 'json', string $
120120
$definition['externalDocs'] = ['url' => $operation->getTypes()[0]];
121121
}
122122

123-
// build ReflectionClass to retrieve ReflectionProperties to detect union/intersect types on buildPropertySchema
124-
try {
125-
$reflectionClass = new \ReflectionClass($className);
126-
} catch (\ReflectionException) {
127-
// class does not exist
128-
$reflectionClass = null;
129-
}
130-
131123
$options = $this->getFactoryOptions($serializerContext, $validationGroups, $operation instanceof HttpOperation ? $operation : null);
132124
foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) {
133125
$propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options);
@@ -140,19 +132,13 @@ public function buildSchema(string $className, string $format = 'json', string $
140132
$definition['required'][] = $normalizedPropertyName;
141133
}
142134

143-
try {
144-
$reflectionProperty = $reflectionClass?->getProperty($propertyName);
145-
} catch (\ReflectionException) {
146-
$reflectionProperty = null;
147-
}
148-
149-
$this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $reflectionProperty);
135+
$this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format);
150136
}
151137

152138
return $schema;
153139
}
154140

155-
private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, ?\ReflectionProperty $reflectionProperty): void
141+
private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format): void
156142
{
157143
$version = $schema->getVersion();
158144
if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) {
@@ -166,6 +152,47 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
166152
$additionalPropertySchema ?? []
167153
);
168154

155+
$types = $propertyMetadata->getBuiltinTypes() ?? [];
156+
157+
// never override the following keys if at least one is already set
158+
// or if property has no type(s) defined
159+
// or if property schema is already fully defined (type=string + format || enum)
160+
$propertySchemaType = $propertySchema['type'] ?? false;
161+
if ([] === $types
162+
|| ($propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false)
163+
|| (\is_array($propertySchemaType) ? \array_key_exists('string', $propertySchemaType) : 'string' !== $propertySchemaType)
164+
|| ($propertySchema['format'] ?? $propertySchema['enum'] ?? false)
165+
) {
166+
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
167+
168+
return;
169+
}
170+
171+
// property schema is created in SchemaPropertyMetadataFactory, but it cannot build resource reference ($ref)
172+
// complete property schema with resource reference ($ref) only if it's related to an object
173+
174+
$version = $schema->getVersion();
175+
$subSchema = new Schema($version);
176+
$subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema
177+
178+
foreach ($types as $type) {
179+
if ($type->isCollection()) {
180+
$valueType = $type->getCollectionValueTypes()[0] ?? null;
181+
} else {
182+
$valueType = $type;
183+
}
184+
185+
$className = $valueType?->getClassName();
186+
if (null === $className || !$this->isResourceClass($className)) {
187+
continue;
188+
}
189+
190+
$subSchema = $this->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, $subSchema, $serializerContext, false);
191+
$propertySchema['anyOf'] = [['$ref' => $subSchema['$ref']], ['type' => 'null']];
192+
unset($propertySchema['type']);
193+
break;
194+
}
195+
169196
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema);
170197
}
171198

tests/Fixtures/DummyResourceImplementation.php renamed to src/JsonSchema/Tests/Fixtures/DummyResourceImplementation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Tests\Fixtures;
14+
namespace ApiPlatform\JsonSchema\Tests\Fixtures;
1515

1616
class DummyResourceImplementation implements DummyResourceInterface
1717
{

tests/Fixtures/DummyResourceInterface.php renamed to src/JsonSchema/Tests/Fixtures/DummyResourceInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Tests\Fixtures;
14+
namespace ApiPlatform\JsonSchema\Tests\Fixtures;
1515

1616
interface DummyResourceInterface
1717
{

tests/Fixtures/NotAResourceWithUnionIntersectTypes.php renamed to src/JsonSchema/Tests/Fixtures/NotAResourceWithUnionIntersectTypes.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Tests\Fixtures;
14+
namespace ApiPlatform\JsonSchema\Tests\Fixtures;
1515

1616
/**
1717
* This class is not mapped as an API resource.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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\JsonSchema\Tests\Fixtures;
15+
16+
interface Serializable
17+
{
18+
public function __serialize(): array;
19+
20+
public function __unserialize(array $data);
21+
}

0 commit comments

Comments
 (0)