diff --git a/src/Schema/Model/Schema.php b/src/Schema/Model/Schema.php index 19e0763..feb73a5 100644 --- a/src/Schema/Model/Schema.php +++ b/src/Schema/Model/Schema.php @@ -10,7 +10,7 @@ readonly class Schema implements JsonSerializable { /** - * @param string|list|null $type + * @param string|array|null $type * @param array|null $properties * @param list|null $required * @param list|null $allOf diff --git a/src/Schema/Parser/OpenApiBuilder.php b/src/Schema/Parser/OpenApiBuilder.php index ec54983..d64bf08 100644 --- a/src/Schema/Parser/OpenApiBuilder.php +++ b/src/Schema/Parser/OpenApiBuilder.php @@ -337,7 +337,7 @@ protected function buildSchema(array $data): Schema description: TypeHelper::asStringOrNull($data['description'] ?? null), default: $data['default'] ?? null, deprecated: (bool) ($data['deprecated'] ?? false), - type: TypeHelper::asStringOrNull($data['type'] ?? null), + type: TypeHelper::asTypeOrNull($data['type'] ?? null), nullable: (bool) ($data['nullable'] ?? false), const: $data['const'] ?? null, multipleOf: TypeHelper::asFloatOrNull($data['multipleOf'] ?? null), diff --git a/src/Validator/Request/BodyParser/BodyParser.php b/src/Validator/Request/BodyParser/BodyParser.php index 4e2fb81..0c7eee1 100644 --- a/src/Validator/Request/BodyParser/BodyParser.php +++ b/src/Validator/Request/BodyParser/BodyParser.php @@ -17,7 +17,7 @@ public function __construct( public function parse(string $body, string $mediaType): array|int|string|float|bool|null { return match ($mediaType) { - 'application/json' => $this->jsonParser->parse($body), + 'application/json', 'application/vnd.api+json' => $this->jsonParser->parse($body), 'application/x-www-form-urlencoded' => $this->formParser->parse($body), 'multipart/form-data' => $this->multipartParser->parse($body), 'text/plain', 'text/html', 'text/csv' => $this->textParser->parse($body), diff --git a/src/Validator/Request/RequestBodyValidator.php b/src/Validator/Request/RequestBodyValidator.php index ddf200b..da9cc7b 100644 --- a/src/Validator/Request/RequestBodyValidator.php +++ b/src/Validator/Request/RequestBodyValidator.php @@ -57,7 +57,7 @@ public function validate( private function parseBody(string $body, string $mediaType): array|int|string|float|bool|null { return match ($mediaType) { - 'application/json' => $this->jsonParser->parse($body), + 'application/json', 'application/vnd.api+json' => $this->jsonParser->parse($body), 'application/x-www-form-urlencoded' => $this->formParser->parse($body), 'multipart/form-data' => $this->multipartParser->parse($body), 'text/plain', 'text/html', 'text/csv' => $this->textParser->parse($body), diff --git a/src/Validator/Request/TypeCoercer.php b/src/Validator/Request/TypeCoercer.php index 724e924..c6991a6 100644 --- a/src/Validator/Request/TypeCoercer.php +++ b/src/Validator/Request/TypeCoercer.php @@ -45,13 +45,13 @@ public function coerce(mixed $value, Parameter $param, bool $enabled, bool $stri } /** - * @param array $types + * @param array $types * @return array|int|string|float|bool */ private function coerceUnionType(mixed $value, array $types, bool $strict): array|int|string|float|bool { foreach ($types as $type) { - if ('null' === $type) { + if ('null' === $type || null === $type) { continue; } diff --git a/src/Validator/Schema/ItemsValidatorWithContext.php b/src/Validator/Schema/ItemsValidatorWithContext.php index b120a56..da9ff2c 100644 --- a/src/Validator/Schema/ItemsValidatorWithContext.php +++ b/src/Validator/Schema/ItemsValidatorWithContext.php @@ -40,7 +40,7 @@ public function validateWithContext(array $data, Schema $schema, ValidationConte /** @var int $index */ $itemContext = $context->withBreadcrumbIndex($index); - $allowNull = $itemSchema->nullable && $context->nullableAsType; + $allowNull = ($itemSchema->nullable && $context->nullableAsType) || SchemaValueNormalizer::typeIncludesNull($itemSchema->type); $normalizedItem = SchemaValueNormalizer::normalize($item, $allowNull); $validator = new SchemaValidatorWithContext($this->pool, $this->refResolver, $this->document); $validator->validateWithContext($normalizedItem, $itemSchema, $itemContext, $useDiscriminator); diff --git a/src/Validator/Schema/PropertiesValidatorWithContext.php b/src/Validator/Schema/PropertiesValidatorWithContext.php index 8929347..fa08489 100644 --- a/src/Validator/Schema/PropertiesValidatorWithContext.php +++ b/src/Validator/Schema/PropertiesValidatorWithContext.php @@ -41,7 +41,7 @@ public function validateWithContext(array $data, Schema $schema, ValidationConte } try { - $allowNull = $propertySchema->nullable && $context->nullableAsType; + $allowNull = ($propertySchema->nullable && $context->nullableAsType) || SchemaValueNormalizer::typeIncludesNull($propertySchema->type); $value = SchemaValueNormalizer::normalize($data[$name], $allowNull); $propertyContext = $context->withBreadcrumb($name); diff --git a/src/Validator/Schema/SchemaValueNormalizer.php b/src/Validator/Schema/SchemaValueNormalizer.php index 1f73e8d..1bb8d47 100644 --- a/src/Validator/Schema/SchemaValueNormalizer.php +++ b/src/Validator/Schema/SchemaValueNormalizer.php @@ -6,6 +6,7 @@ use Duyler\OpenApi\Validator\Exception\InvalidDataTypeException; +use function in_array; use function is_array; use function is_bool; use function is_float; @@ -37,4 +38,16 @@ public static function normalize(mixed $value, bool $allowNull = false): array|i get_debug_type($value), )); } + + /** + * Check if type is an array that includes 'null' or null + */ + public static function typeIncludesNull(string|array|null $type): bool + { + if (is_array($type)) { + return in_array('null', $type, true) || in_array(null, $type, true); + } + + return false; + } } diff --git a/src/Validator/SchemaValidator/AbstractSchemaValidator.php b/src/Validator/SchemaValidator/AbstractSchemaValidator.php index f152282..7e3df24 100644 --- a/src/Validator/SchemaValidator/AbstractSchemaValidator.php +++ b/src/Validator/SchemaValidator/AbstractSchemaValidator.php @@ -5,6 +5,7 @@ namespace Duyler\OpenApi\Validator\SchemaValidator; use Duyler\OpenApi\Validator\Error\ValidationContext; +use Duyler\OpenApi\Validator\Schema\SchemaValueNormalizer; use Duyler\OpenApi\Validator\ValidatorPool; abstract readonly class AbstractSchemaValidator implements SchemaValidatorInterface @@ -21,4 +22,12 @@ protected function getDataPath(?ValidationContext $context): string return $context->breadcrumbs->currentPath(); } + + /** + * Check if type is an array that includes 'null' or null + */ + protected function typeIncludesNull(string|array|null $type): bool + { + return SchemaValueNormalizer::typeIncludesNull($type); + } } diff --git a/src/Validator/SchemaValidator/FormatValidator.php b/src/Validator/SchemaValidator/FormatValidator.php index e9407a5..fbe1f2c 100644 --- a/src/Validator/SchemaValidator/FormatValidator.php +++ b/src/Validator/SchemaValidator/FormatValidator.php @@ -11,6 +11,7 @@ use Override; use function is_array; +use function is_string; readonly class FormatValidator implements SchemaValidatorInterface { @@ -26,7 +27,16 @@ public function validate(mixed $data, Schema $schema, ?ValidationContext $contex return; } - $type = is_array($schema->type) ? $schema->type[0] : $schema->type; + // For union types, get the first non-null string type for format validation + if (is_array($schema->type)) { + $type = array_find($schema->type, fn($t) => is_string($t)); + if (!is_string($type)) { + return; + } + } else { + $type = $schema->type; + } + $formatValidator = $this->formatRegistry->getValidator($type, $schema->format); if (null === $formatValidator) { diff --git a/src/Validator/SchemaValidator/ItemsValidator.php b/src/Validator/SchemaValidator/ItemsValidator.php index 8ad1657..b8b911e 100644 --- a/src/Validator/SchemaValidator/ItemsValidator.php +++ b/src/Validator/SchemaValidator/ItemsValidator.php @@ -33,7 +33,7 @@ public function validate(mixed $data, Schema $schema, ?ValidationContext $contex /** @var int $index */ try { $nullableAsType = $context?->nullableAsType ?? true; - $allowNull = $schema->items->nullable && $nullableAsType; + $allowNull = ($schema->items->nullable && $nullableAsType) || $this->typeIncludesNull($schema->items->type); $normalizedItem = SchemaValueNormalizer::normalize($item, $allowNull); $itemContext = $context?->withBreadcrumbIndex($index) ?? ValidationContext::create($this->pool, $nullableAsType); $validator->validate($normalizedItem, $schema->items, $itemContext); diff --git a/src/Validator/SchemaValidator/PrefixItemsValidator.php b/src/Validator/SchemaValidator/PrefixItemsValidator.php index a9bf369..4bdc70a 100644 --- a/src/Validator/SchemaValidator/PrefixItemsValidator.php +++ b/src/Validator/SchemaValidator/PrefixItemsValidator.php @@ -36,7 +36,7 @@ public function validate(mixed $data, Schema $schema, ?ValidationContext $contex for ($i = 0; $i < $count; ++$i) { try { - $allowNull = $schema->prefixItems[$i]->nullable && $nullableAsType; + $allowNull = ($schema->prefixItems[$i]->nullable && $nullableAsType) || $this->typeIncludesNull($schema->prefixItems[$i]->type); $value = SchemaValueNormalizer::normalize($data[$i], $allowNull); $indexContext = $context?->withBreadcrumbIndex($i) ?? ValidationContext::create($this->pool, $nullableAsType); $validator->validate($value, $schema->prefixItems[$i], $indexContext); @@ -58,7 +58,7 @@ public function validate(mixed $data, Schema $schema, ?ValidationContext $contex if ([] !== $remainingItems && null !== $schema->items) { foreach ($remainingItems as $item) { try { - $allowNull = $schema->items->nullable && $nullableAsType; + $allowNull = ($schema->items->nullable && $nullableAsType) || $this->typeIncludesNull($schema->items->type); $normalizedItem = SchemaValueNormalizer::normalize($item, $allowNull); $remainingContext = $context ?? ValidationContext::create($this->pool, $nullableAsType); $validator->validate($normalizedItem, $schema->items, $remainingContext); diff --git a/src/Validator/SchemaValidator/PropertiesValidator.php b/src/Validator/SchemaValidator/PropertiesValidator.php index d82fb83..9ac4616 100644 --- a/src/Validator/SchemaValidator/PropertiesValidator.php +++ b/src/Validator/SchemaValidator/PropertiesValidator.php @@ -37,7 +37,7 @@ public function validate(mixed $data, Schema $schema, ?ValidationContext $contex try { $nullableAsType = $context?->nullableAsType ?? true; - $allowNull = $propertySchema->nullable && $nullableAsType; + $allowNull = ($propertySchema->nullable && $nullableAsType) || $this->typeIncludesNull($propertySchema->type); $value = SchemaValueNormalizer::normalize($data[$name], $allowNull); $propertyContext = $context?->withBreadcrumb($name) ?? ValidationContext::create($this->pool, $nullableAsType); $validator->validate($value, $propertySchema, $propertyContext); diff --git a/src/Validator/SchemaValidator/TypeValidator.php b/src/Validator/SchemaValidator/TypeValidator.php index 50ef6d1..607865d 100644 --- a/src/Validator/SchemaValidator/TypeValidator.php +++ b/src/Validator/SchemaValidator/TypeValidator.php @@ -73,11 +73,11 @@ private function isValidType(mixed $data, string $type, EmptyArrayStrategy $stra } /** - * @param array $types + * @param array $types */ private function isValidUnionType(mixed $data, array $types, EmptyArrayStrategy $strategy): bool { - return array_any($types, fn($type) => $this->isValidType($data, $type, $strategy)); + return array_any($types, fn($type) => null !== $type && $this->isValidType($data, $type, $strategy)); } private function isArray(mixed $data, EmptyArrayStrategy $strategy): bool diff --git a/tests/Integration/OpenApiV31TypeArraysTest.php b/tests/Integration/OpenApiV31TypeArraysTest.php new file mode 100644 index 0000000..3b2c221 --- /dev/null +++ b/tests/Integration/OpenApiV31TypeArraysTest.php @@ -0,0 +1,243 @@ +psrFactory = new Psr17Factory(); + } + + #[Test] + public function parses_openapi_3_1_type_arrays(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $this->assertSame('3.1.0', $validator->document->openapi); + } + + #[Test] + public function validates_nullable_string_with_null_value(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('GET', '/nullable-primitives'); + $operation = $validator->validateRequest($request); + + $response = $this->psrFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'nullableString' => null, + 'nullableNumber' => 42.5, + 'nullableInteger' => 10, + 'nullableBoolean' => true, + ]))); + + $validator->validateResponse($response, $operation); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function validates_nullable_string_with_string_value(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('GET', '/nullable-primitives'); + $operation = $validator->validateRequest($request); + + $response = $this->psrFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'nullableString' => 'hello', + 'nullableNumber' => null, + 'nullableInteger' => null, + 'nullableBoolean' => null, + ]))); + + $validator->validateResponse($response, $operation); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function validates_nullable_array_with_null_value(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('GET', '/nullable-complex'); + $operation = $validator->validateRequest($request); + + $response = $this->psrFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'nullableArray' => null, + 'nullableObject' => null, + ]))); + + $validator->validateResponse($response, $operation); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function validates_nullable_array_with_array_value(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('GET', '/nullable-complex'); + $operation = $validator->validateRequest($request); + + $response = $this->psrFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'nullableArray' => ['item1', 'item2'], + 'nullableObject' => ['name' => 'test'], + ]))); + + $validator->validateResponse($response, $operation); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function validates_multiple_type_union(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('GET', '/multiple-types'); + $operation = $validator->validateRequest($request); + + $response = $this->psrFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'stringOrNumber' => 'text', + 'stringOrNumberOrNull' => null, + 'stringOrInteger' => 42, + ]))); + + $validator->validateResponse($response, $operation); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function validates_nested_nullable_fields(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('GET', '/nested-nullable'); + $operation = $validator->validateRequest($request); + + $response = $this->psrFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'user' => [ + 'id' => 1, + 'name' => 'John', + 'email' => null, + 'phone' => '+1234567890', + ], + ]))); + + $validator->validateResponse($response, $operation); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function validates_array_with_nullable_items(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('GET', '/array-items-nullable'); + $operation = $validator->validateRequest($request); + + $response = $this->psrFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'item1', null, 'item3', null, 'item5', + ]))); + + $validator->validateResponse($response, $operation); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function validates_request_body_with_type_arrays(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('POST', '/request-body') + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'name' => 'Test Resource', + 'description' => null, + 'age' => 25, + ]))); + + $validator->validateRequest($request); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function backward_compatible_with_nullable_keyword(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $request = $this->psrFactory->createServerRequest('GET', '/backward-compatible'); + $operation = $validator->validateRequest($request); + + $response = $this->psrFactory->createResponse(200) + ->withHeader('Content-Type', 'application/json') + ->withBody($this->psrFactory->createStream(json_encode([ + 'oldStyle' => null, + 'newStyle' => null, + ]))); + + $validator->validateResponse($response, $operation); + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function validates_component_schema_with_type_arrays(): void + { + $validator = OpenApiValidatorBuilder::create() + ->fromYamlFile(__DIR__ . '/../fixtures/v3.1/type-arrays.yaml') + ->build(); + + $data = [ + 'id' => 1, + 'name' => 'John Doe', + 'email' => null, + 'metadata' => null, + ]; + + $validator->validateSchema($data, '#/components/schemas/NullableUser'); + $this->expectNotToPerformAssertions(); + } +} diff --git a/tests/Unit/Schema/Parser/OpenApiBuilderTest.php b/tests/Unit/Schema/Parser/OpenApiBuilderTest.php index 21a9573..470a46b 100644 --- a/tests/Unit/Schema/Parser/OpenApiBuilderTest.php +++ b/tests/Unit/Schema/Parser/OpenApiBuilderTest.php @@ -2211,4 +2211,101 @@ public function accepts_valid_self_uri_file(): void $this->assertSame('file:///path/to/openapi.json', $document->self); } + + #[Test] + public function parses_type_array_with_null(): void + { + $json = json_encode([ + 'openapi' => '3.1.0', + 'info' => ['title' => 'Test', 'version' => '1.0.0'], + 'paths' => [], + 'components' => [ + 'schemas' => [ + 'NullableString' => [ + 'type' => ['string', 'null'], + ], + ], + ], + ]); + + $document = $this->parser->parse($json); + $schema = $document->components->schemas['NullableString']; + + $this->assertIsArray($schema->type); + $this->assertSame(['string', 'null'], $schema->type); + } + + #[Test] + public function parses_type_array_with_multiple_types(): void + { + $json = json_encode([ + 'openapi' => '3.1.0', + 'info' => ['title' => 'Test', 'version' => '1.0.0'], + 'paths' => [], + 'components' => [ + 'schemas' => [ + 'MultiType' => [ + 'type' => ['string', 'number', 'null'], + ], + ], + ], + ]); + + $document = $this->parser->parse($json); + $schema = $document->components->schemas['MultiType']; + + $this->assertIsArray($schema->type); + $this->assertSame(['string', 'number', 'null'], $schema->type); + } + + #[Test] + public function parses_type_array_without_null(): void + { + $json = json_encode([ + 'openapi' => '3.1.0', + 'info' => ['title' => 'Test', 'version' => '1.0.0'], + 'paths' => [], + 'components' => [ + 'schemas' => [ + 'StringOrNumber' => [ + 'type' => ['string', 'number'], + ], + ], + ], + ]); + + $document = $this->parser->parse($json); + $schema = $document->components->schemas['StringOrNumber']; + + $this->assertIsArray($schema->type); + $this->assertSame(['string', 'number'], $schema->type); + } + + #[Test] + public function parses_nested_schema_with_type_array(): void + { + $json = json_encode([ + 'openapi' => '3.1.0', + 'info' => ['title' => 'Test', 'version' => '1.0.0'], + 'paths' => [], + 'components' => [ + 'schemas' => [ + 'User' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'email' => ['type' => ['string', 'null']], + ], + ], + ], + ], + ]); + + $document = $this->parser->parse($json); + $schema = $document->components->schemas['User']; + + $this->assertSame('object', $schema->type); + $this->assertIsArray($schema->properties['email']->type); + $this->assertSame(['string', 'null'], $schema->properties['email']->type); + } } diff --git a/tests/Unit/Validator/Request/RequestBodyValidatorTest.php b/tests/Unit/Validator/Request/RequestBodyValidatorTest.php index d8c4d1a..8748ecf 100644 --- a/tests/Unit/Validator/Request/RequestBodyValidatorTest.php +++ b/tests/Unit/Validator/Request/RequestBodyValidatorTest.php @@ -75,6 +75,36 @@ public function validate_json_body(): void $this->expectNotToPerformAssertions(); } + #[Test] + public function validate_json_api_body(): void + { + $body = '{"data":{"type":"articles","id":"1","attributes":{"title":"Test"}}}'; + $contentType = 'application/vnd.api+json'; + $requestBody = new RequestBody( + content: new Content([ + 'application/vnd.api+json' => new MediaType( + schema: new Schema( + type: 'object', + properties: [ + 'data' => new Schema( + type: 'object', + properties: [ + 'type' => new Schema(type: 'string'), + 'id' => new Schema(type: 'string'), + ], + ), + ], + required: ['data'], + ), + ), + ]), + ); + + $this->validator->validate($body, $contentType, $requestBody); + + $this->expectNotToPerformAssertions(); + } + #[Test] public function validate_form_body(): void { diff --git a/tests/fixtures/v3.1/type-arrays.yaml b/tests/fixtures/v3.1/type-arrays.yaml new file mode 100644 index 0000000..2a0799a --- /dev/null +++ b/tests/fixtures/v3.1/type-arrays.yaml @@ -0,0 +1,161 @@ +openapi: 3.1.0 +info: + title: OpenAPI 3.1 Type Arrays API + version: 1.0.0 + description: Test API for OpenAPI 3.1 type array syntax (replacing nullable keyword) +paths: + /nullable-primitives: + get: + summary: Get nullable primitive types + operationId: getNullablePrimitives + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + nullableString: + type: [string, "null"] + nullableNumber: + type: [number, "null"] + nullableInteger: + type: [integer, "null"] + nullableBoolean: + type: [boolean, "null"] + required: + - nullableString + /nullable-complex: + get: + summary: Get nullable complex types + operationId: getNullableComplex + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + nullableArray: + type: [array, "null"] + items: + type: string + nullableObject: + type: [object, "null"] + properties: + name: + type: string + /multiple-types: + get: + summary: Get multiple type unions + operationId: getMultipleTypes + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + stringOrNumber: + type: [string, number] + stringOrNumberOrNull: + type: [string, number, "null"] + stringOrInteger: + type: [string, integer] + /nested-nullable: + get: + summary: Get nested objects with nullable fields + operationId: getNestedNullable + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + user: + type: object + properties: + id: + type: integer + name: + type: string + email: + type: [string, "null"] + phone: + type: [string, "null"] + required: + - id + - name + /array-items-nullable: + get: + summary: Get array with nullable items + operationId: getArrayItemsNullable + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + type: [string, "null"] + /request-body: + post: + summary: Create resource with nullable fields + operationId: createResource + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: [string, "null"] + age: + type: [integer, "null"] + required: + - name + responses: + '201': + description: Created + /backward-compatible: + get: + summary: Test backward compatibility with nullable keyword + operationId: getBackwardCompatible + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + oldStyle: + type: string + nullable: true + newStyle: + type: [string, "null"] +components: + schemas: + NullableUser: + type: object + properties: + id: + type: integer + name: + type: string + email: + type: [string, "null"] + metadata: + type: [object, "null"] + required: + - id + - name diff --git a/tests/fixtures/v3.2/full-spec.yaml b/tests/fixtures/v3.2/full-spec.yaml index c37b818..e54ca9c 100644 --- a/tests/fixtures/v3.2/full-spec.yaml +++ b/tests/fixtures/v3.2/full-spec.yaml @@ -98,6 +98,8 @@ components: type: integer name: type: string + email: + type: [string, "null"] type: type: string discriminator: @@ -152,10 +154,23 @@ components: enum: [debug, info, warn, error] message: type: string + userId: + type: [integer, "null"] required: - timestamp - level - message + NullableResource: + type: object + properties: + id: + type: integer + name: + type: [string, "null"] + description: + type: [string, "null"] + metadata: + type: [object, "null"] mediaTypes: ProblemJson: schema: