Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Schema/Model/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
readonly class Schema implements JsonSerializable
{
/**
* @param string|list<string>|null $type
* @param string|array<int, string|null>|null $type
* @param array<string, Schema>|null $properties
* @param list<string>|null $required
* @param list<Schema>|null $allOf
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Parser/OpenApiBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion src/Validator/Request/BodyParser/BodyParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion src/Validator/Request/RequestBodyValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 2 additions & 2 deletions src/Validator/Request/TypeCoercer.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ public function coerce(mixed $value, Parameter $param, bool $enabled, bool $stri
}

/**
* @param array<int, string> $types
* @param array<int, string|null> $types
* @return array<array-key, mixed>|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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Validator/Schema/ItemsValidatorWithContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Validator/Schema/PropertiesValidatorWithContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions src/Validator/Schema/SchemaValueNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
9 changes: 9 additions & 0 deletions src/Validator/SchemaValidator/AbstractSchemaValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
12 changes: 11 additions & 1 deletion src/Validator/SchemaValidator/FormatValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Override;

use function is_array;
use function is_string;

readonly class FormatValidator implements SchemaValidatorInterface
{
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/Validator/SchemaValidator/ItemsValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/Validator/SchemaValidator/PrefixItemsValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Validator/SchemaValidator/PropertiesValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/Validator/SchemaValidator/TypeValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ private function isValidType(mixed $data, string $type, EmptyArrayStrategy $stra
}

/**
* @param array<int, string> $types
* @param array<int, string|null> $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
Expand Down
Loading
Loading