diff --git a/src/Handler/UnionHandler.php b/src/Handler/UnionHandler.php index ca6045ad5..660e1dfa3 100644 --- a/src/Handler/UnionHandler.php +++ b/src/Handler/UnionHandler.php @@ -4,9 +4,9 @@ namespace JMS\Serializer\Handler; -use JMS\Serializer\Context; use JMS\Serializer\DeserializationContext; use JMS\Serializer\Exception\NonVisitableTypeException; +use JMS\Serializer\Exception\NotAcceptableException; use JMS\Serializer\Exception\RuntimeException; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\SerializationContext; @@ -50,15 +50,18 @@ public function serializeUnion( SerializationContext $context ): mixed { if ($this->isPrimitiveType(gettype($data))) { - return $this->matchSimpleType($data, $type, $context); + $resolvedType = [ + 'name' => gettype($data), + 'params' => [], + ]; } else { $resolvedType = [ 'name' => get_class($data), 'params' => [], ]; - - return $context->getNavigator()->accept($data, $resolvedType); } + + return $context->getNavigator()->accept($data, $resolvedType); } public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed $data, array $type, DeserializationContext $context): mixed @@ -87,30 +90,27 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed return $context->getNavigator()->accept($data, $finalType); } - foreach ($type['params'][0] as $possibleType) { - if ($this->isPrimitiveType($possibleType['name']) && $this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { - return $context->getNavigator()->accept($data, $possibleType); - } - } + $dataType = gettype($data); - return null; - } + if ( + array_filter( + $type['params'][0], + static fn (array $type): bool => $type['name'] === $dataType || (isset(self::$aliases[$dataType]) && $type['name'] === self::$aliases[$dataType]), + ) + ) { + return $context->getNavigator()->accept($data, [ + 'name' => $dataType, + 'params' => [], + ]); + } - private function matchSimpleType(mixed $data, array $type, Context $context): mixed - { foreach ($type['params'][0] as $possibleType) { - if ($this->isPrimitiveType($possibleType['name']) && !$this->testPrimitive($data, $possibleType['name'], $context->getFormat())) { - continue; - } - - try { + if ($this->isPrimitiveType($possibleType['name']) && $this->testPrimitive($data, $possibleType['name'])) { return $context->getNavigator()->accept($data, $possibleType); - } catch (NonVisitableTypeException $e) { - continue; } } - return null; + throw new NotAcceptableException(); } private function isPrimitiveType(string $type): bool @@ -118,7 +118,7 @@ private function isPrimitiveType(string $type): bool return in_array($type, ['int', 'integer', 'float', 'double', 'bool', 'boolean', 'string', 'array'], true); } - private function testPrimitive(mixed $data, string $type, string $format): bool + private function testPrimitive(mixed $data, string $type): bool { switch ($type) { case 'array': @@ -137,7 +137,7 @@ private function testPrimitive(mixed $data, string $type, string $format): bool return (string) (bool) $data === (string) $data; case 'string': - return is_string($data); + return !is_array($data) && !is_object($data); } return false; diff --git a/tests/Fixtures/TypedProperties/BoolOrString.php b/tests/Fixtures/TypedProperties/BoolOrString.php new file mode 100644 index 000000000..c692f4994 --- /dev/null +++ b/tests/Fixtures/TypedProperties/BoolOrString.php @@ -0,0 +1,15 @@ +data = $data; + } +} diff --git a/tests/Fixtures/TypedProperties/UnionTypedProperties.php b/tests/Fixtures/TypedProperties/UnionTypedProperties.php index d8310e865..bc022f9ff 100644 --- a/tests/Fixtures/TypedProperties/UnionTypedProperties.php +++ b/tests/Fixtures/TypedProperties/UnionTypedProperties.php @@ -6,7 +6,7 @@ class UnionTypedProperties { - private int|bool|float|string|array $data; + public bool|float|string|array|int $data; private int|bool|float|string|null $nullableData; diff --git a/tests/Serializer/JsonSerializationTest.php b/tests/Serializer/JsonSerializationTest.php index f30319dcf..816a1398b 100644 --- a/tests/Serializer/JsonSerializationTest.php +++ b/tests/Serializer/JsonSerializationTest.php @@ -24,6 +24,7 @@ use JMS\Serializer\Tests\Fixtures\ObjectWithInlineArray; use JMS\Serializer\Tests\Fixtures\ObjectWithObjectProperty; use JMS\Serializer\Tests\Fixtures\Tag; +use JMS\Serializer\Tests\Fixtures\TypedProperties\BoolOrString; use JMS\Serializer\Tests\Fixtures\TypedProperties\ComplexDiscriminatedUnion; use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties; use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory; @@ -151,9 +152,12 @@ protected static function getContent($key) $outputs['uninitialized_typed_props'] = '{"virtual_role":{},"id":1,"role":{},"tags":[]}'; $outputs['custom_datetimeinterface'] = '{"custom":"2021-09-07"}'; $outputs['data_integer'] = '{"data":10000}'; + $outputs['data_integer_one'] = '{"data":1}'; $outputs['data_float'] = '{"data":1.236}'; $outputs['data_bool'] = '{"data":false}'; $outputs['data_string'] = '{"data":"foo"}'; + $outputs['data_string_empty'] = '{"data":""}'; + $outputs['data_string_zero'] = '{"data":"0"}'; $outputs['data_array'] = '{"data":[1,2,3]}'; $outputs['data_true'] = '{"data":true}'; $outputs['data_false'] = '{"data":false}'; @@ -446,11 +450,16 @@ public static function getTypeHintedArraysAndStdClass() public static function getSimpleUnionProperties(): iterable { - yield 'int' => [10000, 'data_integer']; + yield [10000, 'data_integer']; yield [1.236, 'data_float']; yield [false, 'data_bool']; yield ['foo', 'data_string']; yield [[1, 2, 3], 'data_array']; + yield [1, 'data_integer_one']; + yield ['0', 'data_string_zero']; + yield ['', 'data_string_empty']; + yield [true, 'data_true']; + yield [false, 'data_false']; } /** @@ -465,9 +474,57 @@ public function testUnionProperties($data, string $expected): void return; } - $object = new UnionTypedProperties($data); - self::assertEquals($object, $this->deserialize(static::getContent($expected), UnionTypedProperties::class)); - self::assertEquals($this->serialize($object), static::getContent($expected)); + $deserialized = $this->deserialize(static::getContent($expected), UnionTypedProperties::class); + + self::assertSame($data, $deserialized->data); + self::assertSame($this->serialize($deserialized), static::getContent($expected)); + } + + public static function getUnionCastableTypes(): iterable + { + yield ['10000', 'data_integer']; + yield ['1.236', 'data_float']; + yield [true, 'data_integer_one']; + } + + /** + * @dataProvider getUnionCastableTypes + */ + #[DataProvider('getUnionCastableTypes')] + public function testUnionPropertiesWithCastableType($data, string $expected) + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $deserialized = $this->deserialize(static::getContent($expected), BoolOrString::class); + + self::assertSame($data, $deserialized->data); + } + + public static function getUnionNotCastableTypes(): iterable + { + yield ['data_array']; + } + + /** + * @dataProvider getUnionNotCastableTypes + */ + #[DataProvider('getUnionNotCastableTypes')] + public function testUnionPropertiesWithNotCastableType(string $expected) + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class)); + + return; + } + + $deserialized = $this->deserialize(static::getContent($expected), BoolOrString::class); + + $this->expectException(\Error::class); + $deserialized->data; } public function testTrueDataType() @@ -482,7 +539,6 @@ public function testTrueDataType() static::getContent('data_true'), $this->serialize(new DataTrue(true)), ); - self::assertEquals( new DataTrue(true), $this->deserialize(static::getContent('data_true'), DataTrue::class), diff --git a/tests/Serializer/JsonStrictSerializationTest.php b/tests/Serializer/JsonStrictSerializationTest.php index c0227fd28..6bdb7f712 100644 --- a/tests/Serializer/JsonStrictSerializationTest.php +++ b/tests/Serializer/JsonStrictSerializationTest.php @@ -4,6 +4,7 @@ namespace JMS\Serializer\Tests\Serializer; +use JMS\Serializer\Exception\NonVisitableTypeException; use JMS\Serializer\SerializerBuilder; use JMS\Serializer\Visitor\Factory\JsonDeserializationVisitorFactory; use PHPUnit\Framework\Attributes\DataProvider; @@ -25,4 +26,15 @@ public function testFirstClassMapCollections(array $items, string $expected): vo { self::markTestIncomplete('Fixtures are broken'); } + + /** + * @dataProvider getUnionCastableTypes + */ + #[DataProvider('getUnionCastableTypes')] + public function testUnionPropertiesWithCastableType($data, string $expected): void + { + $this->expectException(NonVisitableTypeException::class); + + parent::testUnionPropertiesWithCastableType($data, $expected); + } }