Skip to content

Commit ba9ce88

Browse files
test(api): add reproducer for DeserializeProvider enum validation bug
Reproduces the bug that will occur when upgrading to Symfony 8.1. Symfony PR #62574 (commit 35b1aec) improves BackedEnumNormalizer error messages, but API Platform's DeserializeProvider does not handle the case where expectedTypes is null/empty, producing: "This value should be of type ." (empty type). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6bdf289 commit ba9ce88

2 files changed

Lines changed: 103 additions & 4 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Serializer;
6+
7+
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
8+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
9+
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
10+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
11+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
12+
13+
/**
14+
* Reproducer for https://github.com/api-platform/demo/issues/601.
15+
*
16+
* This decorator simulates the behavior introduced in symfony/serializer by
17+
* Symfony PR #62574 (commit 35b1aec), which improves BackedEnumNormalizer
18+
* error messages. That change was temporarily in v8.0.5 (then reverted) and
19+
* will ship in Symfony 8.1.
20+
*
21+
* The improved normalizer distinguishes two error cases:
22+
* 1. Type mismatch: data is not int/string → expectedTypes = [$backingType]
23+
* 2. Invalid value: data is the right type but not a valid enum case
24+
* → expectedTypes = null, message lists valid values
25+
*
26+
* Case 2 exposes a bug in api-platform/state's DeserializeProvider which does
27+
* not handle null/empty expectedTypes, producing: "This value should be of type ."
28+
*
29+
* @todo Remove this decorator once Symfony 8.1 is adopted and the upstream
30+
* API Platform bug is fixed.
31+
*/
32+
#[AsDecorator('serializer.normalizer.backed_enum')]
33+
final class BackedEnumNormalizerDecorator implements NormalizerInterface, DenormalizerInterface
34+
{
35+
public function __construct(
36+
private readonly BackedEnumNormalizer $inner,
37+
) {
38+
}
39+
40+
public function normalize(mixed $data, ?string $format = null, array $context = []): int|string
41+
{
42+
return $this->inner->normalize($data, $format, $context);
43+
}
44+
45+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
46+
{
47+
return $this->inner->supportsNormalization($data, $format, $context);
48+
}
49+
50+
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
51+
{
52+
if (!is_subclass_of($type, \BackedEnum::class)) {
53+
return $this->inner->denormalize($data, $type, $format, $context);
54+
}
55+
56+
$backingType = (new \ReflectionEnum($type))->getBackingType()?->getName();
57+
58+
// Case 1: Type mismatch — data is not the expected backing type
59+
if (null === $data || ('int' === $backingType && !\is_int($data)) || ('string' === $backingType && !\is_string($data))) {
60+
throw NotNormalizableValueException::createForUnexpectedDataType(
61+
\sprintf('The data must be of type %s.', $backingType),
62+
$data,
63+
[$backingType],
64+
$context['deserialization_path'] ?? null,
65+
true,
66+
);
67+
}
68+
69+
// Case 2: Invalid value — right type but not a valid enum case
70+
try {
71+
return $type::from($data);
72+
} catch (\ValueError|\TypeError $e) {
73+
$validValues = array_map(
74+
static fn (\BackedEnum $case): string => \is_string($case->value)
75+
? \sprintf("'%s'", $case->value)
76+
: (string) $case->value,
77+
$type::cases(),
78+
);
79+
80+
throw new NotNormalizableValueException(
81+
message: \sprintf('The data must be one of the following values: %s', implode(', ', $validValues)),
82+
previous: $e,
83+
path: $context['deserialization_path'] ?? null,
84+
useMessageForUser: true,
85+
);
86+
}
87+
}
88+
89+
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
90+
{
91+
return $this->inner->supportsDenormalization($data, $type, $format, $context);
92+
}
93+
94+
public function getSupportedTypes(?string $format): array
95+
{
96+
return $this->inner->getSupportedTypes($format);
97+
}
98+
}

api/tests/Api/Admin/BookTest.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ public static function getInvalidDataOnCreate(): iterable
308308

309309
public static function getInvalidData(): iterable
310310
{
311+
$validValuesHint = "The data must be one of the following values: 'https://schema.org/NewCondition', 'https://schema.org/RefurbishedCondition', 'https://schema.org/DamagedCondition', 'https://schema.org/UsedCondition'";
311312
yield 'empty data' => [
312313
[
313314
'book' => '',
@@ -317,11 +318,11 @@ public static function getInvalidData(): iterable
317318
[
318319
'@type' => 'ConstraintViolation',
319320
'title' => 'An error occurred',
320-
'description' => 'condition: This value should be of type int|string.',
321+
'description' => 'condition: ' . $validValuesHint,
321322
'violations' => [
322323
[
323324
'propertyPath' => 'condition',
324-
'hint' => 'The data must belong to a backed enumeration of type ' . BookCondition::class,
325+
'hint' => $validValuesHint,
325326
],
326327
],
327328
],
@@ -335,11 +336,11 @@ public static function getInvalidData(): iterable
335336
[
336337
'@type' => 'ConstraintViolation',
337338
'title' => 'An error occurred',
338-
'description' => 'condition: This value should be of type int|string.',
339+
'description' => 'condition: ' . $validValuesHint,
339340
'violations' => [
340341
[
341342
'propertyPath' => 'condition',
342-
'hint' => 'The data must belong to a backed enumeration of type ' . BookCondition::class,
343+
'hint' => $validValuesHint,
343344
],
344345
],
345346
],

0 commit comments

Comments
 (0)