API Platform version(s) affected: 4.3.16
Description
When a value of the wrong type is sent for a backed enum property, the 422 validation error reports the property's PHP type (for example Status|null) as the expected type. From an API consumer's point of view this message is useless.
A consumer talks to the API over JSON. For an enum field they send, for example, {"status": "open"}, and the OpenAPI schema documents the field as:
{ "type": "string", "enum": ["open", "closed"] }
When they send the wrong type they now get:
{ "propertyPath": "status", "message": "This value should be of type Status|null." }
Before this regression the same request returned an actionable message that named what a consumer can actually send:
{
"propertyPath": "status",
"message": "This value should be of type int|string.",
"hint": "The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type Status."
}
Status|null is an internal PHP type. It:
- does not appear anywhere in the OpenAPI schema the consumer works from,
- is not something a consumer can send (you send an enum's backing string/int over JSON, not a PHP
Status),
- names none of the accepted values (
open, closed).
So the error is unactionable and it contradicts the schema, which says the field is a string with a fixed set of enum values. Before the TypeInfo migration the same request produced an actionable message naming the accepted scalar (int/string) and the enum, so this is a regression.
The cause is ApiPlatform\Serializer\AbstractItemNormalizer::validateAttributeType(), which validates the attribute against the TypeInfo Type itself and reports (string) $type, instead of letting BackedEnumNormalizer handle enum values:
protected function validateAttributeType(string $attribute, Type $type, mixed $value, ?string $format = null, array $context = []): void
{
if ($type->isIdentifiedBy(TypeIdentifier::FLOAT) && null !== $format && str_contains($format, 'json')) {
$isValid = \is_float($value) || \is_int($value);
} else {
$isValid = $type->accepts($value);
}
if (!$isValid) {
throw NotNormalizableValueException::createForUnexpectedDataType(
\sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $type, \gettype($value)),
$value,
[(string) $type], // "Status|null" for a ?Status property
$context['deserialization_path'] ?? null,
);
}
}
For a ?SomeBackedEnum property, $type->accepts($value) is false for a value of the wrong type, so this throws with expectedTypes = ["SomeBackedEnum|null"] before the value ever reaches BackedEnumNormalizer. With collectDenormalizationErrors enabled that becomes the violation message shown above.
How to reproduce
enum Status: string { case Open = 'open'; case Closed = 'closed'; }
#[ApiResource(operations: [new Post()])]
class Widget { public ?Status $status = null; }
collectDenormalizationErrors: true (the default in many setups).
POST /widgets with {"status": true}.
- Actual:
422, violation message This value should be of type Status|null.
- Expected: a message naming what the consumer can actually send, for example
int|string and how to parse the enum, or the allowed values (open, closed), consistent with the OpenAPI schema.
Possible Solution
For a backed enum attribute (a Type identified by a BackedEnum class), either delegate the type check to BackedEnumNormalizer, or build expectedTypes from the enum's backing type and allowed values rather than from (string) $type, so the error matches the schema and is actionable for an API consumer.
Additional Context
Regression: introduced by the TypeInfo migration in #6979 (which replaced PropertyInfo Type with TypeInfo Type and updated AbstractItemNormalizer); the legacy path was later removed in #8364. On 4.1.x the same request produced a message naming int|string and the enum.
API Platform version(s) affected: 4.3.16
Description
When a value of the wrong type is sent for a backed enum property, the
422validation error reports the property's PHP type (for exampleStatus|null) as the expected type. From an API consumer's point of view this message is useless.A consumer talks to the API over JSON. For an enum field they send, for example,
{"status": "open"}, and the OpenAPI schema documents the field as:{ "type": "string", "enum": ["open", "closed"] }When they send the wrong type they now get:
{ "propertyPath": "status", "message": "This value should be of type Status|null." }Before this regression the same request returned an actionable message that named what a consumer can actually send:
{ "propertyPath": "status", "message": "This value should be of type int|string.", "hint": "The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type Status." }Status|nullis an internal PHP type. It:Status),open,closed).So the error is unactionable and it contradicts the schema, which says the field is a
stringwith a fixed set of enum values. Before the TypeInfo migration the same request produced an actionable message naming the accepted scalar (int/string) and the enum, so this is a regression.The cause is
ApiPlatform\Serializer\AbstractItemNormalizer::validateAttributeType(), which validates the attribute against the TypeInfoTypeitself and reports(string) $type, instead of lettingBackedEnumNormalizerhandle enum values:For a
?SomeBackedEnumproperty,$type->accepts($value)is false for a value of the wrong type, so this throws withexpectedTypes = ["SomeBackedEnum|null"]before the value ever reachesBackedEnumNormalizer. WithcollectDenormalizationErrorsenabled that becomes the violation message shown above.How to reproduce
collectDenormalizationErrors: true(the default in many setups).POST /widgetswith{"status": true}.422, violation messageThis value should be of type Status|null.int|stringand how to parse the enum, or the allowed values (open,closed), consistent with the OpenAPI schema.Possible Solution
For a backed enum attribute (a
Typeidentified by aBackedEnumclass), either delegate the type check toBackedEnumNormalizer, or buildexpectedTypesfrom the enum's backing type and allowed values rather than from(string) $type, so the error matches the schema and is actionable for an API consumer.Additional Context
Regression: introduced by the TypeInfo migration in #6979 (which replaced PropertyInfo
Typewith TypeInfoTypeand updatedAbstractItemNormalizer); the legacy path was later removed in #8364. On 4.1.x the same request produced a message namingint|stringand the enum.