-
-
Notifications
You must be signed in to change notification settings - Fork 960
Description
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 →
Bookentity - 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:
- An option on
stateOptions:validateEntity: true - Automatic when
#[Map(target: Entity::class)]is present on the input DTO - 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?