Skip to content

[Serializer] Discriminated input DTO: object-typed constructor argument declared only on a subclass is passed as a raw array #8414

Description

@aurimasrimkusnfq

API Platform version(s) affected: 4.2.17 - 4.3.17

Description

When an operation uses a polymorphic input DTO - an abstract input class that is not itself #[ApiResource] and carries a #[Serializer\DiscriminatorMap] - a constructor argument that is declared only on a concrete subclass and is object-typed is passed to the constructor as the raw array instead of being denormalized into the object, causing a TypeError (HTTP 500).

Introduced by #7779 (shipped in 4.2.17). Confirmed still present on 4.3.17, the latest stable. Not affected: <= 4.2.16.

All of the following must hold to trigger it:

  1. An operation uses input: <SomeClass>::class where that class is not itself #[ApiResource] (a plain DTO).
  2. That input base carries a #[Serializer\DiscriminatorMap] (polymorphic input).
  3. A mapped concrete subclass declares a constructor argument that is both (a) not a property on the abstract base, and (b) typed as an object / nested DTO (not a scalar or array).
  4. The request payload supplies that field as a nested object.

How to reproduce

use Symfony\Component\Serializer\Attribute\DiscriminatorMap;

// Polymorphic input base - a plain DTO, NOT #[ApiResource].
#[DiscriminatorMap(typeProperty: 'type', mapping: [
    'webhook' => WebhookChannelInput::class,
])]
abstract class NotificationChannelInput
{
}

final class WebhookChannelInput extends NotificationChannelInput
{
    public function __construct(
        public readonly string $type,
        // Object-typed, declared ONLY on this subclass - the trigger.
        public readonly WebhookConfig $config,
    ) {
    }
}

final class WebhookConfig
{
    public function __construct(
        public readonly string $url,
        public readonly int $retries = 3,
    ) {
    }
}
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;

#[ApiResource(operations: [
    new Post(
        uriTemplate: '/notification_channels',
        input: NotificationChannelInput::class,
        // any processor / output
    ),
])]
final class ChannelResource
{
    // ...
}

Request:

POST /notification_channels
Content-Type: application/json

{
  "type": "webhook",
  "config": { "url": "https://example.com/hook", "retries": 5 }
}

Expected: WebhookChannelInput with $config denormalized into a WebhookConfig instance.

Actual: HTTP 500 - WebhookChannelInput::__construct(): Argument #2 ($config) must be of type WebhookConfig, array given

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions