From 4b8580fd9c984619fc84d038a7c14bbb2a31a0c8 Mon Sep 17 00:00:00 2001 From: matlec Date: Mon, 15 Dec 2025 12:45:49 +0100 Subject: [PATCH] [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 {