8000 bug #60413 [Serializer] Fix collect_denormalization_errors flag in de… · symfony/symfony@a880ecb · GitHub
[go: up one dir, main page]

Skip to content

Commit a880ecb

Browse files
bug #60413 [Serializer] Fix collect_denormalization_errors flag in defaultContext (dmbrson)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [Serializer] Fix collect_denormalization_errors flag in defaultContext | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix #59721 | License | MIT When using the `COLLECT_DENORMALIZATION_ERRORS` flag during denormalization, Symfony should collect **all errors** and report them together in a `PartialDenormalizationException`. Here is an example with two expected errors: ```php final readonly class Foo { public function __construct( public string $bar, public \DateTimeInterface $createdAt, ) {} } $foo = $this->denormalizer->denormalize( data: ['createdAt' => ''], type: Foo::class, ); ``` Expected errors 1. `Failed to create object because the class misses the "bar" property.` 2. `The data is either not a string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.` --- When the flag is passed via the `context` ```php $foo = $this->denormalizer->denormalize( data: ['createdAt' => ''], type: Foo::class, context: [ DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, ], ); ``` Both errors are correctly collected and returned. When the flag is set via `default_context` in `framework.yaml`: ```yaml serializer: default_context: collect_denormalization_errors: true ``` Only one error is returned: `The data is either not a string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.` #Root Cause The issue originates in the` \src\Symfony\Component\Serializer\Serializer.php`, `function normalize` : ```php if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) { unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERROR 8000 S]); $context['not_normalizable_value_exceptions'] = []; $errors = &$context['not_normalizable_value_exceptions']; $denormalized = $normalizer->denormalize($data, $type, $format, $context); } ``` The first time this block is hit, it checks for the flag either in $context or $defaultContext. If found, it initializes the error array with: ```php $context['not_normalizable_value_exceptions'] = []; ``` However, during nested denormalization (e.g., when parsing the `createdAt` field), Symfony re-enters this code path. If the flag was provided via `defaultContext`, it is still present on re-entry. Therefore, the `not_normalizable_value_exceptions` array is reset again, losing the previously collected errors. #My Fix The fix is to enhance the condition with an additional check to ensure the array of errors is not already initialized: ```php if ( (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) && !isset($context['not_normalizable_value_exceptions']) ) ``` This ensures the array is only initialized once, preserving previously collected errors in recursive calls, regardless of whether the flag was passed via context or default_context. Commits ------- d4a71ee [Serializer] Fix collect_denormalization_errors flag in defaultContext
2 parents 95c8509 + d4a71ee commit a880ecb

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

src/Symfony/Component/Serializer/Serializer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
222222
throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type));
223223
}
224224

225-
if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) {
225+
if ((isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) && !isset($context['not_normalizable_value_exceptions'])) {
226226
unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]);
227227
$context['not_normalizable_value_exceptions'] = [];
228228
$errors = &$context['not_normalizable_value_exceptions'];

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,54 @@ public function testCollectDenormalizationErrorsDefaultContext()
16771677

16781678
$serializer->denormalize($data, DummyWithVariadicParameter::class);
16791679
}
1680+
1681+
public function testDenormalizationFailsWithMultipleErrorsInDefaultContext()
1682+
{
1683+
$serializer = new Serializer(
1684+
[new DateTimeNormalizer(), new ObjectNormalizer()],
1685+
[],
1686+
[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true]
1687+
);
1688+
1689+
$data = ['date' => '', 'unknown' => null];
1690+
1691+
try {
1692+
$serializer->denormalize($data, DummyEntityWithStringAndDateTime::class);
1693+
$this->fail('Expected PartialDenormalizationException was not thrown');
1694+
} catch (PartialDenormalizationException $e) {
1695+
$this->assertIsArray($e->getErrors());
1696+
$this->assertCount(2, $e->getErrors(), 'Expected two denormalization errors');
1697+
1698+
$exceptionsAsArray = array_map(function (NotNormalizableValueException $ex): array {
1699+
return [
1700+
'currentType' => $ex->getCurrentType(),
1701+
'expectedTypes' => $ex->getExpectedTypes(),
1702+
'path' => $ex->getPath(),
1703+
'useMessageForUser' => $ex->canUseMessageForUser(),
1704+
'message' => $ex->getMessage(),
1705+
];
1706+
}, $e->getErrors());
1707+
1708+
$expected = [
1709+
[
1710+
'currentType' => 'null',
1711+
'expectedTypes' => ['string'],
1712+
'path' => 'bar',
1713+
'useMessageForUser' => true,
1714+
'message' => 'Failed to create object because the class misses the "bar" property.',
1715+
],
1716+
[
1717+
'currentType' => 'string',
1718+
'expectedTypes' => ['string'],
1719+
'path' => 'date',
1720+
'useMessageForUser' => true,
1721+
'message' => 'The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.',
1722+
],
1723+
];
1724+
1725+
$this->assertSame($expected, $exceptionsAsArray);
1726+
}
1727+
}
16801728
}
16811729

16821730
class Model
@@ -1743,6 +1791,15 @@ public function __construct($value)
17431791
}
17441792
}
17451793

1794+
class DummyEntityWithStringAndDateTime
1795+
{
1796+
public function __construct(
1797+
public string $bar,
1798+
public \DateTimeInterface $date,
1799+
) {
1800+
}
1801+
}
1802+
17461803
class DummyUnionType
17471804
{
17481805
/**

0 commit comments

Comments
 (0)
0