Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -1445,12 +1445,16 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value

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

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

throw $denormalizationException;
Expand Down
11 changes: 8 additions & 3 deletions src/State/Provider/DeserializeProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,19 @@ private function normalizeExpectedTypes(?array $expectedTypes = null): array
$normalizedType = $expectedType;

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

$normalizedTypes[] = $normalizedType;
}

return $normalizedTypes;
return array_values(array_unique($normalizedTypes));
}

private function createViolationFromException(NotNormalizableValueException $exception): ConstraintViolation
Expand Down
28 changes: 28 additions & 0 deletions tests/Functional/EnumDenormalizationValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,34 @@ public function testInvalidBackedEnumValueWithCollectDenormalizationErrors(): vo
$this->assertNotNull($this->findViolation($content['violations'] ?? [], 'gender'));
}

/**
* @see https://github.com/api-platform/core/issues/8388
*/
#[IgnoreDeprecations]
public function testWrongTypeForBackedEnumReportsAcceptedScalarTypes(): void
{
if (InstalledVersions::satisfies(new VersionParser(), 'symfony/serializer', '>=8.1')) {
$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.');
}

$response = static::createClient()->request('POST', '/enum_validation_resources_collect', [
'headers' => ['Content-Type' => 'application/ld+json'],
'json' => ['gender' => true],
]);

$this->assertResponseStatusCodeSame(422);

$content = $response->toArray(false);
$genderViolation = $this->findViolation($content['violations'] ?? [], 'gender');
$this->assertNotNull($genderViolation);
// The message must name what a JSON consumer can actually send: the enum's backing scalar
// ("string") and, since the property is nullable, "null" — never the internal PHP type
// "GenderTypeEnum|null", which appears nowhere in the OpenAPI schema.
$this->assertSame('This value should be of type string|null.', $genderViolation['message']);
$this->assertStringNotContainsString('GenderTypeEnum', $genderViolation['message']);
$this->assertStringContainsString('enumeration case of type', $genderViolation['hint'] ?? '');
}

private function findViolation(array $violations, string $propertyPath): ?array
{
foreach ($violations as $violation) {
Expand Down
Loading