diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 43a8ac5e07..253f1a0b30 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -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; diff --git a/src/State/Provider/DeserializeProvider.php b/src/State/Provider/DeserializeProvider.php index 338c837141..e264cc19e0 100644 --- a/src/State/Provider/DeserializeProvider.php +++ b/src/State/Provider/DeserializeProvider.php @@ -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 diff --git a/tests/Functional/EnumDenormalizationValidationTest.php b/tests/Functional/EnumDenormalizationValidationTest.php index 3fa939623c..5b6e8cd587 100644 --- a/tests/Functional/EnumDenormalizationValidationTest.php +++ b/tests/Functional/EnumDenormalizationValidationTest.php @@ -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) {