Skip to content

[GraphQL] PHP 8.1 backed enums not properly deserialized from JavaScript/TypeScript client #7009

Open
@lcottingham

Description

@lcottingham

API Platform version(s) affected: 4.x

Description
There is an issue with PHP 8.1 backed enums when used in GraphQL operations with JavaScript/TypeScript clients. While API Platform correctly generates GraphQL schema introspection for PHP backed enums, it uses the basic EnumType class from webonyx/graphql-php instead of the specialized PhpEnumType class.

This causes a validation error when sending requests from JavaScript/TypeScript clients because JSON serialization always converts enum values to strings (with quotes), but the EnumType class expects raw enum values (without quotes).

For example, when a TypeScript enum is sent in a GraphQL mutation:

enum OrderStatus {
  PENDING = "PENDING",
  PROCESSING = "PROCESSING",
  COMPLETED = "COMPLETED"
}

// In client code:
createOrder({ status: OrderStatus.PENDING }) 
// This becomes { "status": "PENDING" } in JSON

API Platform rejects this with an error:

Enum "OrderStatusEnum" cannot represent non-enum value: "PENDING". Did you mean the enum value "PENDING"?

The error occurs because API Platform uses EnumType instead of PhpEnumType, and EnumType doesn't handle string values automatically.

How to reproduce

  1. Create a PHP 8.1 backed enum:
<?php
namespace App\Enum;

enum OrderStatus: string
{
    case PENDING = 'PENDING';
    case PROCESSING = 'PROCESSING';
    case COMPLETED = 'COMPLETED';
}
  1. Use it in an API Resource:
<?php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;
use App\Enum\OrderStatus;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(
    graphQlOperations: [
        new Mutation(name: 'create', args: [
            'status' => ['type' => OrderStatus::class]
        ])
    ]
)]
class Order
{
    #[ORM\Column(type: 'string', enumType: OrderStatus::class)]
    private OrderStatus $status;
    
    // Other properties, getters, setters...
}
  1. Create a TypeScript/JavaScript GraphQL client that uses the enum:
const CREATE_ORDER = gql`
  mutation CreateOrder($status: OrderStatusEnum!) {
    createOrder(input: { status: $status }) {
      order { id }
    }
  }
`;

// This will fail:
client.mutate({
  mutation: CREATE_ORDER,
  variables: { status: "PENDING" }
});

The request fails with the error message described above, even though "PENDING" is a valid enum value.

Possible Solution
The root cause is in API Platform's enum type building implementation which uses EnumType rather than PhpEnumType for PHP backed enums.

This seems to be handled in the TypeBuilder.php getEnumType() method.

The key difference between these classes is in their construction:

EnumType is constructed with a configuration array:

$enumType = new EnumType([
    'name' => $enumName,
    'values' => $enumCases,
    // ...
]);

While PhpEnumType is specifically designed for PHP 8.1 enums and is constructed differently:

/**
 * @param class-string<\UnitEnum> $enumClass The fully qualified class name of a native PHP enum
 * @param string|null $name The name the enum will have in the schema, defaults to the basename of the given class
 * @param string|null $description The description the enum will have in the schema, defaults to PHPDoc of the given class
 * @param array<EnumTypeExtensionNode>|null $extensionASTNodes
 *
 * @throws \Exception
 * @throws \ReflectionException
 */
$enumType = new PhpEnumType(
    OrderStatus::class, // The enum class name (must be a UnitEnum)
    $name, // Optional name (defaults to basename of class)
    $description // Optional description (defaults to PHPDoc)
);

PhpEnumType is purpose-built for PHP 8.1 enums - it automatically extracts enum cases from the class through reflection and, most importantly, handles the string-to-enum conversion properly:

public function __construct(
    string $enumClass,
    ?string $name = null,
    ?string $description = null,
    // ...
) {
    $this->enumClass = $enumClass;
    $reflection = new \ReflectionEnum($enumClass);

    // Extract enum cases automatically
    $enumDefinitions = [];
    foreach ($reflection->getCases() as $case) {
        $enumDefinitions[$case->name] = [
            'value' => $case->getValue(),
            // ...
        ];
    }

    parent::__construct([
        'name' => $name ?? $this->baseName($enumClass),
        'values' => $enumDefinitions,
        // ...
    ]);
}

Due to these constructor differences, a solution would need to:

  1. Identify when a GraphQL type represents a PHP 8.1 enum
  2. Use PhpEnumType with the enum class name instead of building an EnumType with a config array

This could be implemented by decorating the appropriate API Platform type builder service to use PhpEnumType when appropriate.

Additional Context
This issue only affects GraphQL operations when using JavaScript/TypeScript clients. REST operations work correctly because they use Symfony's serializer which properly handles enum deserialization.

The author of webonyx/graphql-php confirmed that the PhpEnumType class is specifically designed to handle this case, as evidenced by the tests in:

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions