Skip to content

Symfony 7.4 Validate mapped entity when using ObjectMapper with stateOptions #7725

@julian-douma

Description

@julian-douma

Description

When using input: DTO::class with #[Map(target: Entity::class)] and stateOptions: new Options(entityClass: Entity::class), validation only runs on the input DTO. Entity-level constraints like #[UniqueEntity] are never triggered, causing a 500 (DB constraint violation) instead of a proper 422 validation error.

How to reproduce

Entity with UniqueEntity constraint:

#[ORM\Entity]
#[UniqueEntity(fields: ['isbn'], message: 'A book with this ISBN already exists.')]
class Book
{
    #[ORM\Column(type: 'string', length: 13, unique: true)]
    #[Assert\Isbn(type: Assert\Isbn::ISBN_13)]
    private string $isbn;
    
    // ...
}

Input DTO mapped to Entity:

#[Map(target: Book::class)]
final class CreateBookDto
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Isbn(type: Assert\Isbn::ISBN_13)]
        public readonly string $isbn,
        // ...
    ) {}
}

API Resource:

#[ApiResource(
    stateOptions: new Options(entityClass: Book::class),
    operations: [
        new Post(
            uriTemplate: '/books',
            input: CreateBookDto::class,
        ),
    ],
)]
#[Map(source: Book::class)]
class BookResource
{
    // ...
}

Request:

# First request - succeeds
curl -X POST /api/books -d '{"isbn": "9780134685991", ...}'

# Second request with same ISBN - returns 500 instead of 422
curl -X POST /api/books -d '{"isbn": "9780134685991", ...}'

Expected behavior

The #[UniqueEntity] constraint on the Entity should be validated after ObjectMapper maps the DTO to the Entity, returning a 422 validation error.

Actual behavior

  • Validation runs on CreateBookDto (passes - no uniqueness check)
  • ObjectMapper maps DTO → Book entity
  • Entity is persisted → DB throws constraint violation (500)

The #[UniqueEntity] constraint is never evaluated.

Current workaround

Custom processor that manually checks for duplicates:

final class CreateBookProcessor implements ProcessorInterface
{
    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Book
    {
        $existing = $this->entityManager
            ->getRepository(Book::class)
            ->findOneBy(['isbn' => $data->getIsbn()]);

        if ($existing !== null) {
            throw new DuplicateIsbnException($data->getIsbn());
        }

        $this->entityManager->persist($data);
        $this->entityManager->flush();

        return $data;
    }
}

Possible solution

Since #[Map] already declares the DTO → Entity relationship, validation could automatically run on the mapped entity before persistence. This could be:

  1. An option on stateOptions: validateEntity: true
  2. Automatic when #[Map(target: Entity::class)] is present on the input DTO
  3. A new processor in the chain that validates the entity post-mapping

Environment

  • API Platform 4.x
  • Symfony 7.x
  • Using ObjectMapper for DTO ↔ Entity mapping

Maybe I am doing something wrong?

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