From 4b8580fd9c984619fc84d038a7c14bbb2a31a0c8 Mon Sep 17 00:00:00 2001 From: matlec Date: Mon, 15 Dec 2025 12:45:49 +0100 Subject: [PATCH 1/2] [Serializer] Do not skip nested `null` values when denormalizing --- .../Normalizer/AbstractObjectNormalizer.php | 16 +++++++------ .../AbstractObjectNormalizerTest.php | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 8a6581641abff..015699b759cb4 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException as PropertyAccessInvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -333,15 +334,16 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $nestedAttributes = $this->getNestedAttributes($mappedClass); $nestedData = $originalNestedData = []; - $propertyAccessor = PropertyAccess::createPropertyAccessor(); + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()->enableExceptionOnInvalidIndex()->getPropertyAccessor(); foreach ($nestedAttributes as $property => $serializedPath) { - if (null === $value = $propertyAccessor->getValue($normalizedData, $serializedPath)) { - continue; + try { + $value = $propertyAccessor->getValue($normalizedData, $serializedPath); + $convertedProperty = $this->nameConverter ? $this->nameConverter->normalize($property, $mappedClass, $format, $context) : $property; + $nestedData[$convertedProperty] = $value; + $originalNestedData[$property] = $value; + $normalizedData = $this->removeNestedValue($serializedPath->getElements(), $normalizedData); + } catch (NoSuchIndexException) { } - $convertedProperty = $this->nameConverter ? $this->nameConverter->normalize($property, $mappedClass, $format, $context) : $property; - $nestedData[$convertedProperty] = $value; - $originalNestedData[$property] = $value; - $normalizedData = $this->removeNestedValue($serializedPath->getElements(), $normalizedData); } $normalizedData = $nestedData + $normalizedData; diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 441f15752b492..d8c7d89a24ba4 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -850,6 +850,29 @@ public function testDenormalizeWithCorrectOrderOfAttributeAndProperty() $this->assertSame('nested-id', $test->id); } + public function testDenormalizeMissingAndNullNestedValues() + { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + + $data = [ + 'data' => [ + 'foo' => null, + ], + ]; + + $obj = new class { + #[SerializedPath('[data][foo]')] + public ?string $foo; + + #[SerializedPath('[data][bar]')] + public ?string $bar; + }; + + $test = $normalizer->denormalize($data, $obj::class); + $this->assertNull($test->foo); + $this->assertFalse((new \ReflectionProperty($obj, 'bar'))->isInitialized($obj)); + } + public function testNormalizeBasedOnAllowedAttributes() { $normalizer = new class extends AbstractObjectNormalizer { From 02e3153c295dcfc9a06cf29cff96cf70fb5ed665 Mon Sep 17 00:00:00 2001 From: Hamza Makraz Date: Mon, 15 Dec 2025 02:45:24 +0100 Subject: [PATCH 2/2] [PropertyInfo] Fix calling same-named method with required args instead of reading public property --- .../PropertyAccess/Tests/PropertyAccessorTest.php | 15 +++++++++++++++ .../Extractor/ReflectionExtractor.php | 6 ++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index a0caa1e12e2ce..9636cb4cd85bb 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -52,6 +52,21 @@ protected function setUp(): void $this->propertyAccessor = new PropertyAccessor(); } + public function testPrefersPropertyOverMethodWithSameNameAndRequiredArgs() + { + $obj = new class { + public bool $loaded = true; + + // Same name as property, but requires an argument: must NOT be called for reading + public function loaded(string $arg): bool + { + throw new \RuntimeException('Method should not be invoked during property read'); + } + }; + + $this->assertTrue($this->propertyAccessor->getValue($obj, 'loaded')); + } + public static function getPathsWithMissingProperty() { return [ diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 9c9d65b61cdc3..421b860f64ef3 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -270,8 +270,10 @@ public function getReadInfo(string $class, string $property, array $context = [] if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($reflClass->getMethod($getsetter)->getModifiers() & $this->methodReflectionFlags)) { $method = $reflClass->getMethod($getsetter); - - return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisibilityForMethod($method), $method->isStatic(), false); + // Only consider jQuery-style accessors when they don't require parameters + if (!$method->getNumberOfRequiredParameters()) { + return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisibilityForMethod($method), $method->isStatic(), false); + } } if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) {