Skip to content

Commit bf4b2b0

Browse files
committed
Better nullable properties in exported OpenAPI
1 parent f52a17e commit bf4b2b0

File tree

5 files changed

+98
-63
lines changed

5 files changed

+98
-63
lines changed

src/JsonSchema/SchemaFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
214214
$className = $valueType->getClassName();
215215
}
216216

217-
$valueSchema = $this->typeFactory->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection), $format, $propertyMetadata->isReadableLink(), $serializerContext, $schema);
217+
$valueSchema = $this->typeFactory->getType(new Type($builtinType, !$propertyMetadata->isRequired() || $type->isNullable(), $className, $isCollection), $format, $propertyMetadata->isReadableLink(), $serializerContext, $schema);
218218
}
219219

220220
$propertySchema = new \ArrayObject($propertySchema + $valueSchema);

src/JsonSchema/TypeFactory.php

+17-11
Original file line numberDiff line numberDiff line change
@@ -146,21 +146,27 @@ private function getClassType(?string $className, string $format, ?bool $readabl
146146
*/
147147
private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type, ?Schema $schema): array
148148
{
149-
if ($schema && Schema::VERSION_SWAGGER === $schema->getVersion()) {
149+
if (!$schema || !$type->isNullable()) {
150150
return $jsonSchema;
151151
}
152152

153-
if (!$type->isNullable()) {
154-
return $jsonSchema;
155-
}
156-
157-
if (\array_key_exists('$ref', $jsonSchema)) {
158-
return [
159-
'nullable' => true,
160-
'anyOf' => [$jsonSchema],
161-
];
153+
switch ($schema->getVersion()) {
154+
case Schema::VERSION_SWAGGER:
155+
if (!\array_key_exists('$ref', $jsonSchema)) {
156+
return array_merge($jsonSchema, ['type' => [$jsonSchema['type'], 'null']]);
157+
}
158+
break;
159+
default:
160+
if (\array_key_exists('$ref', $jsonSchema)) {
161+
return [
162+
'nullable' => true,
163+
'anyOf' => [$jsonSchema],
164+
];
165+
}
166+
167+
return array_merge($jsonSchema, ['nullable' => true]);
162168
}
163169

164-
return array_merge($jsonSchema, ['nullable' => true]);
170+
return $jsonSchema;
165171
}
166172
}

tests/JsonSchema/TypeFactoryTest.php

+19-19
Original file line numberDiff line numberDiff line change
@@ -152,31 +152,31 @@ public function testGetTypeWithOpenAPIV2Syntax(array $schema, Type $type): void
152152

153153
public function openAPIV2typeProvider(): iterable
154154
{
155-
yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)];
156-
yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT, true)];
155+
yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT, false)];
156+
yield [['type' => ['integer', 'null']], new Type(Type::BUILTIN_TYPE_INT, true)];
157157
yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)];
158-
yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT, true)];
158+
yield [['type' => ['number', 'null']], new Type(Type::BUILTIN_TYPE_FLOAT, true)];
159159
yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)];
160-
yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL, true)];
160+
yield [['type' => ['boolean', 'null']], new Type(Type::BUILTIN_TYPE_BOOL, true)];
161161
yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)];
162-
yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING, true)];
162+
yield [['type' => ['string', 'null']], new Type(Type::BUILTIN_TYPE_STRING, true)];
163163
yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT)];
164-
yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT, true)];
164+
yield [['type' => ['string', 'null']], new Type(Type::BUILTIN_TYPE_OBJECT, true)];
165165
yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)];
166-
yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)];
166+
yield [['type' => ['string', 'null'], 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)];
167167
yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)];
168168
yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)];
169-
yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)];
169+
yield [['type' => ['string', 'null'], 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)];
170170
yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)];
171-
yield 'array can be itself nullable, but ignored in OpenAPI V2' => [
172-
['type' => 'array', 'items' => ['type' => 'string']],
171+
yield 'array can be itself nullable' => [
172+
['type' => ['array', 'null'], 'items' => ['type' => 'string']],
173173
new Type(Type::BUILTIN_TYPE_STRING, true, null, true),
174174
];
175175

176-
yield 'array can contain nullable values, but ignored in OpenAPI V2' => [
176+
yield 'array can contain nullable values' => [
177177
[
178178
'type' => 'array',
179-
'items' => ['type' => 'string'],
179+
'items' => ['type' => ['string', 'null']],
180180
],
181181
new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)),
182182
];
@@ -192,9 +192,9 @@ public function openAPIV2typeProvider(): iterable
192192
),
193193
];
194194

195-
yield 'nullable map with string keys becomes a nullable object, but ignored in OpenAPI V2' => [
195+
yield 'nullable map with string keys becomes a nullable object' => [
196196
[
197-
'type' => 'object',
197+
'type' => ['object', 'null'],
198198
'additionalProperties' => ['type' => 'string'],
199199
],
200200
new Type(
@@ -219,10 +219,10 @@ public function openAPIV2typeProvider(): iterable
219219
),
220220
];
221221

222-
yield 'map value type nullability will be considered, but ignored in OpenAPI V2' => [
222+
yield 'map value type nullability will be considered' => [
223223
[
224224
'type' => 'object',
225-
'additionalProperties' => ['type' => 'integer'],
225+
'additionalProperties' => ['type' => ['integer', 'null']],
226226
],
227227
new Type(
228228
Type::BUILTIN_TYPE_ARRAY,
@@ -234,10 +234,10 @@ public function openAPIV2typeProvider(): iterable
234234
),
235235
];
236236

237-
yield 'nullable map can contain nullable values, but ignored in OpenAPI V2' => [
237+
yield 'nullable map can contain nullable values' => [
238238
[
239-
'type' => 'object',
240-
'additionalProperties' => ['type' => 'integer'],
239+
'type' => ['object', 'null'],
240+
'additionalProperties' => ['type' => ['integer', 'null']],
241241
],
242242
new Type(
243243
Type::BUILTIN_TYPE_ARRAY,

0 commit comments

Comments
 (0)