From aa44e57062f9318df9b84606b01ec42f7d7f2683 Mon Sep 17 00:00:00 2001 From: Cyril LUSSIANA Date: Fri, 10 Apr 2026 17:04:21 +0200 Subject: [PATCH] feat: improve union types [improve-union-types] - allow null type - replace union oneOf by anyOf to prevent integer / number overlap for int|float typing --- src/Draft04/Schema.php | 11 ++++--- tests/GenerateJsonSchemaTest.php | 54 +++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/Draft04/Schema.php b/src/Draft04/Schema.php index a575eec..f286a5d 100644 --- a/src/Draft04/Schema.php +++ b/src/Draft04/Schema.php @@ -557,6 +557,9 @@ public static function inferType( case 'stdClass': $propertySchema = new ObjectSchema(); break; + case 'null': + $propertySchema = new NullSchema(); + break; case 'mixed': $propertySchema = new Schema(); break; @@ -604,7 +607,7 @@ enumClass: $enumName } } - if ($typeReflection->allowsNull()) { + if ($typeReflection->allowsNull() && $typeReflection->getName() !== 'null') { $propertySchema = new Schema( oneOf: [ new NullSchema(), @@ -617,13 +620,13 @@ enumClass: $enumName } if ($typeReflection instanceof ReflectionUnionType) { - $oneOf = []; + $anyOf = []; foreach ($typeReflection->getTypes() as $subTypeReflection) { - $oneOf[] = static::inferType($current, $root, $currentClassReflection, $subTypeReflection); + $anyOf[] = static::inferType($current, $root, $currentClassReflection, $subTypeReflection); } return new Schema( - oneOf: $oneOf + anyOf: $anyOf ); } diff --git a/tests/GenerateJsonSchemaTest.php b/tests/GenerateJsonSchemaTest.php index 07bbd67..9030a1b 100644 --- a/tests/GenerateJsonSchemaTest.php +++ b/tests/GenerateJsonSchemaTest.php @@ -273,6 +273,14 @@ public function __construct( } } +final class ForceInferNullableUnion +{ + public function __construct( + public float|int|null $value, + ) { + } +} + final class GenerateJsonSchemaTest extends TestCase { public function testBasicSchema(): void @@ -588,7 +596,7 @@ public function testDraft04Schema(): void ], 'height' => [ 'default' => 180, - 'oneOf' => [ + 'anyOf' => [ [ 'type' => 'string' ], @@ -802,4 +810,48 @@ public function testForceInfer04(): void Draft04Schema::classSchema(ForceInfer::class, forceInfer: true)->jsonSerialize(), ); } + + public function testForceInferNullableUnion04(): void + { + $rawSchema = [ + '$schema' => Draft::Draft04->value, + 'type' => 'object', + 'properties' => [ + 'value' => [ + 'anyOf' => [ + [ + 'type' => 'integer' + ], + [ + 'type' => 'number' + ], + [ + 'type' => 'null' + ], + ] + ] + ], + 'required' => [ 'value' ] + ]; + + $this->assertEquals( + $rawSchema, + Draft04Schema::classSchema(ForceInferNullableUnion::class, forceInfer: true)->jsonSerialize(), + ); + + $ast = (new Generator)->generateSchema($rawSchema); + $reconstructed = eval('return ' . (new PrettyPrinter\Standard())->prettyPrintExpr($ast) . ';'); + + $this->assertInstanceOf(Draft04Schema::class, $reconstructed); + $this->assertEquals($rawSchema, $reconstructed->jsonSerialize()); + + $entities = (new Draft04EntityGenerator(new Trunk($rawSchema), namespace: 'Test'))->generateEntities(ForceInferNullableUnion::class); + + eval((new PrettyPrinter\Standard())->prettyPrint($entities)); + + $this->assertEquals( + $rawSchema, + Draft04Schema::classSchema(Test\ForceInferNullableUnion::class, forceInfer: true)->jsonSerialize(), + ); + } }