From f4630a7dd27a57d30aa840225c18790a8a3561a2 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Thu, 20 Feb 2020 00:14:35 +0100 Subject: [PATCH] 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: https://github.com/api-platform/core/pull/3402#issuecomment-588518155 --- src/JsonSchema/TypeFactory.php | 31 ++-------- tests/JsonSchema/TypeFactoryTest.php | 58 +++++++++++++++---- .../DocumentationNormalizerV2Test.php | 11 +++- .../DocumentationNormalizerV3Test.php | 11 +++- 4 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php index f3e758b8ce9..8bd1c6d86bf 100644 --- a/src/JsonSchema/TypeFactory.php +++ b/src/JsonSchema/TypeFactory.php @@ -17,9 +17,6 @@ use ApiPlatform\Core\Util\ResourceClassInfoTrait; use Ramsey\Uuid\UuidInterface; use Symfony\Component\PropertyInfo\Type; -use function array_merge; -use function array_unique; -use function array_values; /** * {@inheritdoc} @@ -163,27 +160,11 @@ private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): return $jsonSchema; } - if (!\array_key_exists('type', $jsonSchema)) { - return [ - 'oneOf' => [ - ['type' => 'null'], - $jsonSchema, - ], - ]; - } - - return array_merge($jsonSchema, ['type' => $this->addNullToTypes((array) $jsonSchema['type'])]); - } - - /** - * @param string[] $types - * - * @return string[] - * - * @psalm-param list $types - */ - private function addNullToTypes(array $types): array - { - return array_values(array_unique(array_merge($types, ['null']))); + return [ + 'oneOf' => [ + ['type' => 'null'], + $jsonSchema, + ], + ]; } } diff --git a/tests/JsonSchema/TypeFactoryTest.php b/tests/JsonSchema/TypeFactoryTest.php index 4f2aaad3495..4bef70f28b0 100644 --- a/tests/JsonSchema/TypeFactoryTest.php +++ b/tests/JsonSchema/TypeFactoryTest.php @@ -35,28 +35,36 @@ public function testGetType(array $schema, Type $type): void public function typeProvider(): iterable { yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)]; - yield [['type' => ['integer', 'null']], new Type(Type::BUILTIN_TYPE_INT, true)]; + yield [['oneOf' => [['type' => 'null'], ['type' => 'integer']]], new Type(Type::BUILTIN_TYPE_INT, true)]; yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)]; - yield [['type' => ['number', 'null']], new Type(Type::BUILTIN_TYPE_FLOAT, true)]; + yield [['oneOf' => [['type' => 'null'], ['type' => 'number']]], new Type(Type::BUILTIN_TYPE_FLOAT, true)]; yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)]; - yield [['type' => ['boolean', 'null']], new Type(Type::BUILTIN_TYPE_BOOL, true)]; + yield [['oneOf' => [['type' => 'null'], ['type' => 'boolean']]], new Type(Type::BUILTIN_TYPE_BOOL, true)]; yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)]; - yield [['type' => ['string', 'null']], new Type(Type::BUILTIN_TYPE_STRING, true)]; + yield [['oneOf' => [['type' => 'null'], ['type' => 'string']]], new Type(Type::BUILTIN_TYPE_STRING, true)]; yield [['type' => 'object'], new Type(Type::BUILTIN_TYPE_OBJECT)]; - yield [['type' => ['object', 'null']], new Type(Type::BUILTIN_TYPE_OBJECT, true)]; + yield [['oneOf' => [['type' => 'null'], ['type' => 'object']]], new Type(Type::BUILTIN_TYPE_OBJECT, true)]; yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]; - yield [['type' => ['string', 'null'], 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)]; + yield [['oneOf' => [['type' => 'null'], ['type' => 'string', 'format' => 'date-time']]], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)]; yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)]; yield [['type' => 'object'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; - yield [['type' => ['object', 'null']], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; + yield [['oneOf' => [['type' => 'null'], ['type' => 'object']]], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ - ['type' => ['array', 'null'], 'items' => ['type' => 'string']], + ['oneOf' => [['type' => 'null'], ['type' => 'array', 'items' => ['type' => 'string']]]], new Type(Type::BUILTIN_TYPE_STRING, true, null, true), ]; yield 'array can contain nullable values' => [ - ['type' => 'array', 'items' => ['type' => ['string', 'null']]], + [ + 'type' => 'array', + 'items' => [ + 'oneOf' => [ + ['type' => 'null'], + ['type' => 'string'], + ], + ], + ], new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)), ]; @@ -72,7 +80,12 @@ public function typeProvider(): iterable ]; yield 'nullable map with string keys becomes a nullable object' => [ - ['type' => ['object', 'null'], 'additionalProperties' => ['type' => 'string']], + [ + 'oneOf' => [ + ['type' => 'null'], + ['type' => 'object', 'additionalProperties' => ['type' => 'string']], + ], + ], new Type( Type::BUILTIN_TYPE_STRING, true, @@ -96,7 +109,15 @@ public function typeProvider(): iterable ]; yield 'map value type nullability will be considered' => [ - ['type' => 'object', 'additionalProperties' => ['type' => ['integer', 'null']]], + [ + 'type' => 'object', + 'additionalProperties' => [ + 'oneOf' => [ + ['type' => 'null'], + ['type' => 'integer'], + ], + ], + ], new Type( Type::BUILTIN_TYPE_ARRAY, false, @@ -108,7 +129,20 @@ public function typeProvider(): iterable ]; yield 'nullable map can contain nullable values' => [ - ['type' => ['object', 'null'], 'additionalProperties' => ['type' => ['integer', 'null']]], + [ + 'oneOf' => [ + ['type' => 'null'], + [ + 'type' => 'object', + 'additionalProperties' => [ + 'oneOf' => [ + ['type' => 'null'], + ['type' => 'integer'], + ], + ], + ], + ], + ], new Type( Type::BUILTIN_TYPE_ARRAY, true, diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php index 25fe3931610..5ae6dab93de 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV2Test.php @@ -343,9 +343,14 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'description' => 'This is an initializable but not writable property.', ]), 'dummyDate' => new \ArrayObject([ - 'type' => ['string', 'null'], - 'description' => 'This is a \DateTimeInterface object.', - 'format' => 'date-time', + 'oneOf' => [ + ['type' => 'null'], + [ + 'type' => 'string', + 'description' => 'This is a \DateTimeInterface object.', + 'format' => 'date-time', + ], + ], ]), ], ]), diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php index c7a741ed70d..76102115402 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php @@ -385,9 +385,14 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth 'description' => 'This is an initializable but not writable property.', ]), 'dummyDate' => new \ArrayObject([ - 'type' => ['string', 'null'], - 'description' => 'This is a \DateTimeInterface object.', - 'format' => 'date-time', + 'oneOf' => [ + ['type' => 'null'], + [ + 'type' => 'string', + 'description' => 'This is a \DateTimeInterface object.', + 'format' => 'date-time', + ], + ], ]), ], ]),