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_ERRORS]); $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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy