Skip to content

Commit

Permalink
Using oneOf also for simple non-$ref types
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
Ocramius committed Feb 19, 2020
1 parent b41362a commit 6b6c40d
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 41 deletions.
31 changes: 6 additions & 25 deletions src/JsonSchema/TypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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<string> $types
*/
private function addNullToTypes(array $types): array
{
return array_values(array_unique(array_merge($types, ['null'])));
return [
'oneOf' => [
['type' => 'null'],
$jsonSchema,
],
];
}
}
58 changes: 46 additions & 12 deletions tests/JsonSchema/TypeFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
];

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions tests/Swagger/Serializer/DocumentationNormalizerV2Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,14 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth
'description' => 'This is an initializable but not writable property.',
]),
'dummyDate' => new \ArrayObject([
'type' => ['string', 'null'],
'oneOf' => [
['type' => 'null'],
[
'type' => 'string',
'format' => 'date-time',
],
],
'description' => 'This is a \DateTimeInterface object.',
'format' => 'date-time',
]),
],
]),
Expand Down
9 changes: 7 additions & 2 deletions tests/Swagger/Serializer/DocumentationNormalizerV3Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,14 @@ private function doTestNormalize(OperationMethodResolverInterface $operationMeth
'description' => 'This is an initializable but not writable property.',
]),
'dummyDate' => new \ArrayObject([
'type' => ['string', 'null'],
'oneOf' => [
['type' => 'null'],
[
'type' => 'string',
'format' => 'date-time',
],
],
'description' => 'This is a \DateTimeInterface object.',
'format' => 'date-time',
]),
],
]),
Expand Down

0 comments on commit 6b6c40d

Please sign in to comment.