Skip to content

Commit b396ff9

Browse files
authored
fix(serializer): report enum backing type in denormalization violations (#8389)
Closes #8388
1 parent 079c746 commit b396ff9

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)