From 14d5ccf7888e52c9ea9e67a036c3ae996630ccdb Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Mon, 8 Sep 2025 16:54:16 +0200 Subject: [PATCH] Fix data types of enum values in Json Schema --- fixtures/StructuredOutput/ExampleDto.php | 24 ++++++++++++++++++ src/platform/phpstan.dist.neon | 1 + .../Contract/JsonSchema/Attribute/With.php | 9 ++++--- .../Attribute/ToolParameterTest.php | 6 ++--- .../tests/Contract/JsonSchema/FactoryTest.php | 25 +++++++++++++++++++ 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 fixtures/StructuredOutput/ExampleDto.php diff --git a/fixtures/StructuredOutput/ExampleDto.php b/fixtures/StructuredOutput/ExampleDto.php new file mode 100644 index 000000000..2d2ec6844 --- /dev/null +++ b/fixtures/StructuredOutput/ExampleDto.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Fixtures\StructuredOutput; + +use Symfony\AI\Platform\Contract\JsonSchema\Attribute\With; + +class ExampleDto +{ + public function __construct( + public string $name, + #[With(enum: [7, 19])] public int $taxRate, + #[With(enum: ['Foo', 'Bar', null])] public ?string $category, + ) { + } +} diff --git a/src/platform/phpstan.dist.neon b/src/platform/phpstan.dist.neon index 860d59d77..0e4373282 100644 --- a/src/platform/phpstan.dist.neon +++ b/src/platform/phpstan.dist.neon @@ -7,6 +7,7 @@ parameters: paths: - src/ - tests/ + treatPhpDocTypesAsCertain: false ignoreErrors: - message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#" diff --git a/src/platform/src/Contract/JsonSchema/Attribute/With.php b/src/platform/src/Contract/JsonSchema/Attribute/With.php index 4ceb70576..cc311c5f4 100644 --- a/src/platform/src/Contract/JsonSchema/Attribute/With.php +++ b/src/platform/src/Contract/JsonSchema/Attribute/With.php @@ -20,8 +20,8 @@ final readonly class With { /** - * @param list|null $enum - * @param string|int|string[]|null $const + * @param list|null $enum + * @param string|int|string[]|null $const */ public function __construct( // can be used by many types @@ -53,8 +53,9 @@ public function __construct( public ?bool $dependentRequired = null, ) { if (\is_array($enum)) { - if (array_filter($enum, fn ($item) => \is_string($item)) !== $enum) { - throw new InvalidArgumentException('All enum values must be strings.'); + /* @phpstan-ignore-next-line function.alreadyNarrowedType */ + if (array_filter($enum, fn (mixed $item) => null === $item || \is_int($item) || \is_float($item) || \is_string($item)) !== $enum) { + throw new InvalidArgumentException('All enum values must be float, integer, strings, or null.'); } } diff --git a/src/platform/tests/Contract/JsonSchema/Attribute/ToolParameterTest.php b/src/platform/tests/Contract/JsonSchema/Attribute/ToolParameterTest.php index 8f21ed01f..bc15a54f8 100644 --- a/src/platform/tests/Contract/JsonSchema/Attribute/ToolParameterTest.php +++ b/src/platform/tests/Contract/JsonSchema/Attribute/ToolParameterTest.php @@ -26,11 +26,11 @@ public function testValidEnum() $this->assertSame($enum, $toolParameter->enum); } - public function testInvalidEnumContainsNonString() + public function testInvalidEnumContainsInvalidType() { $this->expectException(InvalidArgumentException::class); - $enum = ['value1', 2]; - new With(enum: $enum); + $enum = ['value1', new \stdClass()]; + new With(enum: $enum); /* @phpstan-ignore-line argument.type */ } public function testValidConstString() diff --git a/src/platform/tests/Contract/JsonSchema/FactoryTest.php b/src/platform/tests/Contract/JsonSchema/FactoryTest.php index d9e50a23a..8ca6cb72b 100644 --- a/src/platform/tests/Contract/JsonSchema/FactoryTest.php +++ b/src/platform/tests/Contract/JsonSchema/FactoryTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Fixtures\StructuredOutput\ExampleDto; use Symfony\AI\Fixtures\StructuredOutput\MathReasoning; use Symfony\AI\Fixtures\StructuredOutput\Step; use Symfony\AI\Fixtures\StructuredOutput\User; @@ -240,4 +241,28 @@ public function testBuildPropertiesForStepClass() $this->assertSame($expected, $actual); } + + public function testBuildPropertiesForExampleDto() + { + $expected = [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'taxRate' => [ + 'type' => 'integer', + 'enum' => [7, 19], + ], + 'category' => [ + 'type' => ['string', 'null'], + 'enum' => ['Foo', 'Bar', null], + ], + ], + 'required' => ['name', 'taxRate'], + 'additionalProperties' => false, + ]; + + $actual = $this->factory->buildProperties(ExampleDto::class); + + $this->assertSame($expected, $actual); + } }