Skip to content

Commit f4630a7

Browse files
committed
Using oneOf also for simple non-$ref types
As per OpenAPI documentation at https://swagger.io/docs/specification/data-models/data-types/ the OpenAPI v3 documentation does not allow defining union types via `type: ['string', 'integer']`, but requires explicit usage of `oneOf` and nested type declarations or references instead. Ref: #3402 (comment)
1 parent a68808d commit f4630a7

File tree

4 files changed

+68
-43
lines changed

4 files changed

+68
-43
lines changed

src/JsonSchema/TypeFactory.php

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
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;
2320

2421
/**
2522
* {@inheritdoc}
@@ -163,27 +160,11 @@ private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type):
163160
return $jsonSchema;
164161
}
165162

166-
if (!\array_key_exists('type', $jsonSchema)) {
167-
return [
168-
'oneOf' => [
169-
['type' => 'null'],
170-
$jsonSchema,
171-
],
172-
];
173-
}
174-
175-
return array_merge($jsonSchema, ['type' => $this->addNullToTypes((array) $jsonSchema['type'])]);
176-
}
177-
178-
/**
179-
* @param string[] $types
180-
*
181-
* @return string[]
182-
*
183-
* @psalm-param list<string> $types
184-
*/
185-
private function addNullToTypes(array $types): array
186-
{
187-
return array_values(array_unique(array_merge($types, ['null'])));
163+
return [
164+
'oneOf' => [
165+
['type' => 'null'],
166+
$jsonSchema,
167+
],
168+
];
188169
}
189170
}

tests/JsonSchema/TypeFactoryTest.php

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,36 @@ public function testGetType(array $schema, Type $type): void
3535
public function typeProvider(): iterable
3636
{
3737
yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)];
38-
yield [['type' => ['integer', 'null']], new Type(Type::BUILTIN_TYPE_INT, true)];
38+
yield [['oneOf' => [['type' => 'null'], ['type' => 'integer']]], new Type(Type::BUILTIN_TYPE_INT, true)];
3939
yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)];
40-
yield [['type' => ['number', 'null']], new Type(Type::BUILTIN_TYPE_FLOAT, true)];
40+
yield [['oneOf' => [['type' => 'null'], ['type' => 'number']]], new Type(Type::BUILTIN_TYPE_FLOAT, true)];
4141
yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)];
42-
yield [['type' => ['boolean', 'null']], new Type(Type::BUILTIN_TYPE_BOOL, true)];
42+
yield [['oneOf' => [['type' => 'null'], ['type' => 'boolean']]], new Type(Type::BUILTIN_TYPE_BOOL, true)];
4343
yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)];
44-
yield [['type' => ['string', 'null']], new Type(Type::BUILTIN_TYPE_STRING, true)];
44+
yield [['oneOf' => [['type' => 'null'], ['type' => 'string']]], new Type(Type::BUILTIN_TYPE_STRING, true)];
4545
yield [['type' => 'object'], new Type(Type::BUILTIN_TYPE_OBJECT)];
46-
yield [['type' => ['object', 'null']], new Type(Type::BUILTIN_TYPE_OBJECT, true)];
46+
yield [['oneOf' => [['type' => 'null'], ['type' => 'object']]], new Type(Type::BUILTIN_TYPE_OBJECT, true)];
4747
yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)];
48-
yield [['type' => ['string', 'null'], 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)];
48+
yield [['oneOf' => [['type' => 'null'], ['type' => 'string', 'format' => 'date-time']]], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)];
4949
yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)];
5050
yield [['type' => 'object'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)];
51-
yield [['type' => ['object', 'null']], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)];
51+
yield [['oneOf' => [['type' => 'null'], ['type' => 'object']]], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)];
5252
yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)];
5353
yield 'array can be itself nullable' => [
54-
['type' => ['array', 'null'], 'items' => ['type' => 'string']],
54+
['oneOf' => [['type' => 'null'], ['type' => 'array', 'items' => ['type' => 'string']]]],
5555
new Type(Type::BUILTIN_TYPE_STRING, true, null, true),
5656
];
5757

5858
yield 'array can contain nullable values' => [
59-
['type' => 'array', 'items' => ['type' => ['string', 'null']]],
59+
[
60+
'type' => 'array',
61+
'items' => [
62+
'oneOf' => [
63+
['type' => 'null'],
64+
['type' => 'string'],
65+
],
66+
],
67+
],
6068
new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)),
6169
];
6270

@@ -72,7 +80,12 @@ public function typeProvider(): iterable
7280
];
7381

7482
yield 'nullable map with string keys becomes a nullable object' => [
75-
['type' => ['object', 'null'], 'additionalProperties' => ['type' => 'string']],
83+
[
84+
'oneOf' => [
85+
['type' => 'null'],
86+
['type' => 'object', 'additionalProperties' => ['type' => 'string']],
87+
],
88+
],
7689
new Type(
7790
Type::BUILTIN_TYPE_STRING,
7891
true,
@@ -96,7 +109,15 @@ public function typeProvider(): iterable
96109
];
97110

98111
yield 'map value type nullability will be considered' => [
99-
['type' => 'object', 'additionalProperties' => ['type' => ['integer', 'null']]],
112+
[
113+
'type' => 'object',
114+
'additionalProperties' => [
115+
'oneOf' => [
116+
['type' => 'null'],
117+
['type' => 'integer'],
118+
],
119+
],
120+
],
100121
new Type(
101122
Type::BUILTIN_TYPE_ARRAY,
102123
false,
@@ -108,7 +129,20 @@ public function typeProvider(): iterable
108129
];
109130

110131
yield 'nullable map can contain nullable values' => [
111-
['type' => ['object', 'null'], 'additionalProperties' => ['type' => ['integer', 'null']]],
132+
[
133+
'oneOf' => [
134+
['type' => 'null'],
135+
[
136+
'type' => 'object',
137+
'additionalProperties' => [
138+
'oneOf' => [
139+
['type' => 'null'],
140+
['type' => 'integer'],
141+
],
142+
],
143+
],
144+
],
145+
],
112146
new Type(
113147
Type::BUILTIN_TYPE_ARRAY,
114148
true,

tests/Swagger/Serializer/DocumentationNormalizerV2Test.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,14 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth
343343
'description' => 'This is an initializable but not writable property.',
344344
]),
345345
'dummyDate' => new \ArrayObject([
346-
'type' => ['string', 'null'],
347-
'description' => 'This is a \DateTimeInterface object.',
348-
'format' => 'date-time',
346+
'oneOf' => [
347+
['type' => 'null'],
348+
[
349+
'type' => 'string',
350+
'description' => 'This is a \DateTimeInterface object.',
351+
'format' => 'date-time',
352+
],
353+
],
349354
]),
350355
],
351356
]),

tests/Swagger/Serializer/DocumentationNormalizerV3Test.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,14 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth
385385
'description' => 'This is an initializable but not writable property.',
386386
]),
387387
'dummyDate' => new \ArrayObject([
388-
'type' => ['string', 'null'],
389-
'description' => 'This is a \DateTimeInterface object.',
390-
'format' => 'date-time',
388+
'oneOf' => [
389+
['type' => 'null'],
390+
[
391+
'type' => 'string',
392+
'description' => 'This is a \DateTimeInterface object.',
393+
'format' => 'date-time',
394+
],
395+
],
391396
]),
392397
],
393398
]),

0 commit comments

Comments
 (0)