From d839c13a311da066c109110d802185261bac3f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8n=20Gaillard?= Date: Wed, 28 Oct 2026 14:08:32 +0100 Subject: [PATCH 1/2] Throw NotNormalizableValueException for empty or invalid dates. This is the expected exception for cases like that, and allows APIs to natively return 422 when the normalizer fails to normalize null or empty values. Co-Authored-By: Claude Opus 5.5 --- src/Normalizer/AbsoluteDateNormalizer.php | 23 +++++++--- .../Normalizer/AbsoluteDateNormalizerTest.php | 43 +++++++++++++++++-- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/Normalizer/AbsoluteDateNormalizer.php b/src/Normalizer/AbsoluteDateNormalizer.php index aaaa021..a5147b1 100644 --- a/src/Normalizer/AbsoluteDateNormalizer.php +++ b/src/Normalizer/AbsoluteDateNormalizer.php @@ -6,7 +6,7 @@ use AssoConnect\PHPDate\AbsoluteDate; use Symfony\Component\Serializer\Exception\InvalidArgumentException; -use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Throwable; @@ -52,16 +52,29 @@ public function supportsNormalization(mixed $data, ?string $format = null, array */ public function denormalize($data, string $type, ?string $format = null, array $context = []): AbsoluteDate { + $dateTimeFormat = $context[self::FORMAT_KEY] ?? AbsoluteDate::DEFAULT_DATE_FORMAT; + if ('' === $data || null === $data) { - throw new UnexpectedValueException(); + throw NotNormalizableValueException::createForUnexpectedDataType( + sprintf('The data is empty, a date in format "%s" is expected.', $dateTimeFormat), + $data, + [AbsoluteDate::class], + $context['deserialization_path'] ?? null, + true, + ); } - $dateTimeFormat = $context[self::FORMAT_KEY] ?? AbsoluteDate::DEFAULT_DATE_FORMAT; - try { return new AbsoluteDate($data, $dateTimeFormat); } catch (Throwable $e) { - throw new UnexpectedValueException(previous: $e); + throw NotNormalizableValueException::createForUnexpectedDataType( + sprintf('The data is not a valid date in format "%s".', $dateTimeFormat), + $data, + [AbsoluteDate::class], + $context['deserialization_path'] ?? null, + true, + previous: $e, + ); } } diff --git a/tests/Normalizer/AbsoluteDateNormalizerTest.php b/tests/Normalizer/AbsoluteDateNormalizerTest.php index d445e8e..c1cb414 100644 --- a/tests/Normalizer/AbsoluteDateNormalizerTest.php +++ b/tests/Normalizer/AbsoluteDateNormalizerTest.php @@ -6,8 +6,10 @@ use AssoConnect\PHPDate\AbsoluteDate; use AssoConnect\PHPDateBundle\Normalizer\AbsoluteDateNormalizer; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; class AbsoluteDateNormalizerTest extends TestCase @@ -70,15 +72,48 @@ public function testDenormalizeUsingFormatPassedInContext(): void ); } - public function testDenormalizeInvalidDataThrowsException(): void + /** + * @return iterable + */ + public static function provideNotNormalizableData(): iterable { - $this->expectException(UnexpectedValueException::class); - $this->normalizer->denormalize('invalid date', AbsoluteDate::class); + yield 'null' => [null, 'The data is empty, a date in format "Y-m-d" is expected.']; + yield 'empty string' => ['', 'The data is empty, a date in format "Y-m-d" is expected.']; + yield 'invalid date' => ['invalid date', 'The data is not a valid date in format "Y-m-d".']; + } + + #[DataProvider('provideNotNormalizableData')] + public function testDenormalizeNotNormalizableDataThrowsException(mixed $data, string $expectedMessage): void + { + try { + $this->normalizer->denormalize($data, AbsoluteDate::class, null, [ + 'deserialization_path' => 'startDate', + ]); + self::fail('A NotNormalizableValueException was expected.'); + } catch (NotNormalizableValueException $exception) { + self::assertInstanceOf(UnexpectedValueException::class, $exception); + self::assertSame($expectedMessage, $exception->getMessage()); + self::assertSame([AbsoluteDate::class], $exception->getExpectedTypes()); + self::assertSame('startDate', $exception->getPath()); + self::assertTrue($exception->canUseMessageForUser()); + } + } + + public function testDenormalizeInvalidDataChainsThePreviousException(): void + { + try { + $this->normalizer->denormalize('invalid date', AbsoluteDate::class); + self::fail('A NotNormalizableValueException was expected.'); + } catch (NotNormalizableValueException $exception) { + self::assertNotNull($exception->getPrevious()); + self::assertNull($exception->getPath()); + } } public function testDenormalizeFormatMismatchThrowsException(): void { - $this->expectException(UnexpectedValueException::class); + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('The data is not a valid date in format "Y-m-d|".'); $this->normalizer->denormalize('2016/01/01', AbsoluteDate::class, null, [ AbsoluteDateNormalizer::FORMAT_KEY => 'Y-m-d|', ]); From edddb25b3de8545831cad1316add95dd29af552f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8n=20Gaillard?= Date: Thu, 29 Oct 2026 10:45:10 +0100 Subject: [PATCH 2/2] Drop the always-true UnexpectedValueException assertio --- tests/Normalizer/AbsoluteDateNormalizerTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Normalizer/AbsoluteDateNormalizerTest.php b/tests/Normalizer/AbsoluteDateNormalizerTest.php index c1cb414..8a0987d 100644 --- a/tests/Normalizer/AbsoluteDateNormalizerTest.php +++ b/tests/Normalizer/AbsoluteDateNormalizerTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; -use Symfony\Component\Serializer\Exception\UnexpectedValueException; class AbsoluteDateNormalizerTest extends TestCase { @@ -91,7 +90,6 @@ public function testDenormalizeNotNormalizableDataThrowsException(mixed $data, s ]); self::fail('A NotNormalizableValueException was expected.'); } catch (NotNormalizableValueException $exception) { - self::assertInstanceOf(UnexpectedValueException::class, $exception); self::assertSame($expectedMessage, $exception->getMessage()); self::assertSame([AbsoluteDate::class], $exception->getExpectedTypes()); self::assertSame('startDate', $exception->getPath());