Skip to content

Commit 39b278c

Browse files
committed
fix(serializer): report enum backing type in denormalization violations
When a wrong-typed value is sent for a nullable backed enum property (e.g. `?Status`), collectDenormalizationErrors produced the violation message "This value should be of type Status|null." — the internal PHP type, which appears nowhere in the OpenAPI schema and names none of the accepted values. AbstractItemNormalizer delegates nullable backed enums to BackedEnumNormalizer, which throws an actionable exception (accepted scalars + hint). The post-loop block then rewrapped it, replacing its expectedTypes with `(string) $types` ("Status|null") and dropping the user-facing hint flag. Preserve the hint flag when the failure comes from an object/enum normalizer, and in DeserializeProvider render a BackedEnum expected type as its JSON-visible backing scalar ("string"/"int") instead of the enum FQCN. The enum class stays in expectedTypes so the 400->422 promotion (#8183) keeps working. Now `{"status": true}` on a `?Status` property yields "This value should be of type string|null." plus the enum hint. Closes #8388
1 parent 079c746 commit 39b278c

3 files changed

Lines changed: 43 additions & 6 deletions

File tree

src/Serializer/AbstractItemNormalizer.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,12 +1445,16 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value
14451445

14461446
if ($denormalizationException) {
14471447
if ($type instanceof Type && $type->isSatisfiedBy(static fn ($type) => $type instanceof BuiltinType) && !$type->isSatisfiedBy($typeIsResourceClass)) {
1448-
// If the exception came from object denormalization, preserve its message as it's more specific
1449-
$message = $type->isSatisfiedBy(static fn ($type) => $type instanceof ObjectType)
1448+
// If the exception came from object denormalization (e.g. BackedEnumNormalizer for a
1449+
// nullable backed enum), preserve its more specific message and its user-facing hint flag;
1450+
// the expected types still carry the object/enum class so the failure stays recognizable
1451+
// downstream (see DeserializeProvider).
1452+
$isObject = $type->isSatisfiedBy(static fn ($type) => $type instanceof ObjectType);
1453+
$message = $isObject
14501454
? $denormalizationException->getMessage()
14511455
: \sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $type, \gettype($value));
14521456

1453-
throw NotNormalizableValueException::createForUnexpectedDataType($message, $value, array_map(strval(...), $types), $context['deserialization_path'] ?? null, false, 0, $denormalizationException);
1457+
throw NotNormalizableValueException::createForUnexpectedDataType($message, $value, array_map(strval(...), $types), $context['deserialization_path'] ?? null, $isObject && $denormalizationException->canUseMessageForUser(), 0, $denormalizationException);
14541458
}
14551459

14561460
throw $denormalizationException;

src/State/Provider/DeserializeProvider.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,19 @@ private function normalizeExpectedTypes(?array $expectedTypes = null): array
143143
$normalizedType = $expectedType;
144144

145145
if (class_exists($expectedType) || interface_exists($expectedType)) {
146-
$classReflection = new \ReflectionClass($expectedType);
147-
$normalizedType = $classReflection->getShortName();
146+
// A backed enum is sent over the wire as its backing scalar (e.g. "string"), not as the
147+
// PHP enum class, so report the JSON-visible type rather than the internal FQCN (#8388).
148+
if (is_subclass_of($expectedType, \BackedEnum::class) && ($backingType = (new \ReflectionEnum($expectedType))->getBackingType())) {
149+
$normalizedType = (string) $backingType;
150+
} else {
151+
$normalizedType = (new \ReflectionClass($expectedType))->getShortName();
152+
}
148153
}
149154

150155
$normalizedTypes[] = $normalizedType;
151156
}
152157

153-
return $normalizedTypes;
158+
return array_values(array_unique($normalizedTypes));
154159
}
155160

156161
private function createViolationFromException(NotNormalizableValueException $exception): ConstraintViolation

tests/Functional/EnumDenormalizationValidationTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,34 @@ public function testInvalidBackedEnumValueWithCollectDenormalizationErrors(): vo
8484
$this->assertNotNull($this->findViolation($content['violations'] ?? [], 'gender'));
8585
}
8686

87+
/**
88+
* @see https://github.com/api-platform/core/issues/8388
89+
*/
90+
#[IgnoreDeprecations]
91+
public function testWrongTypeForBackedEnumReportsAcceptedScalarTypes(): void
92+
{
93+
if (InstalledVersions::satisfies(new VersionParser(), 'symfony/serializer', '>=8.1')) {
94+
$this->expectUserDeprecationMessage('Since symfony/serializer 8.1: The "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getErrors()" method is deprecated, use "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getNotNormalizableValueErrors()" instead.');
95+
}
96+
97+
$response = static::createClient()->request('POST', '/enum_validation_resources_collect', [
98+
'headers' => ['Content-Type' => 'application/ld+json'],
99+
'json' => ['gender' => true],
100+
]);
101+
102+
$this->assertResponseStatusCodeSame(422);
103+
104+
$content = $response->toArray(false);
105+
$genderViolation = $this->findViolation($content['violations'] ?? [], 'gender');
106+
$this->assertNotNull($genderViolation);
107+
// The message must name what a JSON consumer can actually send: the enum's backing scalar
108+
// ("string") and, since the property is nullable, "null" — never the internal PHP type
109+
// "GenderTypeEnum|null", which appears nowhere in the OpenAPI schema.
110+
$this->assertSame('This value should be of type string|null.', $genderViolation['message']);
111+
$this->assertStringNotContainsString('GenderTypeEnum', $genderViolation['message']);
112+
$this->assertStringContainsString('enumeration case of type', $genderViolation['hint'] ?? '');
113+
}
114+
87115
private function findViolation(array $violations, string $propertyPath): ?array
88116
{
89117
foreach ($violations as $violation) {

0 commit comments

Comments
 (0)