Skip to content

Commit affdd7d

Browse files
Ocramiusteohhanhui
authored andcommitted
Corrected schema generation for array<string, T>, object, ?array, ?object and ?type
The previous version of the `TypeFactory` generated following **WRONG** definitions: * `null|T[]` as `{"type": array, "items": {"type": "T"}}` * `?T[]` as `{"type": array, "items": {"type": "T"}}` * `array<string, T> as `{"type": array, "items": {"type": "T"}}` * `object` without explicit schema definition as `{"type": "string"}` * `?T` as `{"type": T}` The new definitions instead do fix this by mapping: * `array<string, T>` as `{"type": "object", "additionalProperties": {"type": "T"}}` * `array<string, ?T> as `{"type": object, "additionalProperties": {"type": ["T", "null"]}}` * `null|array<string, T>` as `{"type": ["object", "null"], "additionalProperties": {"type": "T"}}` * `array<int, T>` as `{"type": "array", "items": {"type": "T"}}` (not very precise, but list support is not yet in symfony) * `object` without explicit schema definition as `{"type": "object"}` * `?T[]` as `{"type": "array", "items": {"type": ["T", "null"]}}` * `null|T[]` as `{"type": ["array", "null"], "items": {"type": "T"}}`
1 parent 94be0cc commit affdd7d

File tree

1 file changed

+73
-8
lines changed

1 file changed

+73
-8
lines changed

TypeFactory.php

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
1818
use Ramsey\Uuid\UuidInterface;
1919
use Symfony\Component\PropertyInfo\Type;
20+
use function array_merge;
21+
use function array_unique;
22+
use function array_values;
2023

2124
/**
2225
* {@inheritdoc}
@@ -50,14 +53,37 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
5053
public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array
5154
{
5255
if ($type->isCollection()) {
53-
$subType = new Type($type->getBuiltinType(), $type->isNullable(), $type->getClassName(), false);
54-
55-
return [
56-
'type' => 'array',
57-
'items' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema),
58-
];
56+
$keyType = $type->getCollectionKeyType();
57+
$subType = $type->getCollectionValueType()
58+
?? new Type($type->getBuiltinType(), false, $type->getClassName(), false);
59+
60+
if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) {
61+
return $this->addNullabilityToTypeDefinition(
62+
[
63+
'type' => 'object',
64+
'additionalProperties' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema),
65+
],
66+
$type
67+
);
68+
}
69+
70+
return $this->addNullabilityToTypeDefinition(
71+
[
72+
'type' => 'array',
73+
'items' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema),
74+
],
75+
$type
76+
);
5977
}
6078

79+
return $this->addNullabilityToTypeDefinition(
80+
$this->makeBasicType($type, $format, $readableLink, $serializerContext, $schema),
81+
$type
82+
);
83+
}
84+
85+
private function makeBasicType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array
86+
{
6187
switch ($type->getBuiltinType()) {
6288
case Type::BUILTIN_TYPE_INT:
6389
return ['type' => 'integer'];
@@ -78,7 +104,7 @@ public function getType(Type $type, string $format = 'json', ?bool $readableLink
78104
private function getClassType(?string $className, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array
79105
{
80106
if (null === $className) {
81-
return ['type' => 'string'];
107+
return ['type' => 'object'];
82108
}
83109

84110
if (is_a($className, \DateTimeInterface::class, true)) {
@@ -102,7 +128,7 @@ private function getClassType(?string $className, string $format = 'json', ?bool
102128

103129
// Skip if $schema is null (filters only support basic types)
104130
if (null === $schema) {
105-
return ['type' => 'string'];
131+
return ['type' => 'object'];
106132
}
107133

108134
if ($this->isResourceClass($className) && true !== $readableLink) {
@@ -125,4 +151,43 @@ private function getClassType(?string $className, string $format = 'json', ?bool
125151

126152
return ['$ref' => $subSchema['$ref']];
127153
}
154+
155+
/**
156+
* @param array<string, mixed> $jsonSchema
157+
*
158+
* @return array<string, mixed>
159+
*
160+
* @psalm-param array{type=: string|list<string>} $jsonSchema
161+
*
162+
* @psalm-return array{type=: string|list<string>, $ref=: string}
163+
*/
164+
private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): array
165+
{
166+
if (!$type->isNullable()) {
167+
return $jsonSchema;
168+
}
169+
170+
if (!\array_key_exists('type', $jsonSchema)) {
171+
return [
172+
'oneOf' => [
173+
['type' => 'null'],
174+
$jsonSchema,
175+
],
176+
];
177+
}
178+
179+
return array_merge($jsonSchema, ['type' => $this->addNullToTypes((array) $jsonSchema['type'])]);
180+
}
181+
182+
/**
183+
* @param string[] $types
184+
*
185+
* @return string[]
186+
*
187+
* @psalm-param list<string> $types
188+
*/
189+
private function addNullToTypes(array $types): array
190+
{
191+
return array_values(array_unique(array_merge($types, ['null'])));
192+
}
128193
}

0 commit comments

Comments
 (0)