Skip to content

Denormalization error for a backed enum property reports the PHP type Status|null instead of an accepted value #8388

Description

@bendavies

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; }
  1. collectDenormalizationErrors: true (the default in many setups).
  2. POST /widgets with {"status": true}.
  3. Actual: 422, violation message This value should be of type Status|null.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions