Introduction
Handling validation errors is something every Symfony API needs — yet it often ends up messy, repetitive, and inconsistent.
Modern Symfony makes request validation elegant with DTOs and attributes like #[MapRequestPayload].
But when validation fails, the default error-handling story still isn’t great.
In this post, I’ll show:
- The common problem with validation errors in Symfony APIs
- Why the default behavior doesn’t scale well
- A clean, modern approach to solving it
- How to get consistent JSON validation responses with zero boilerplate
The Common Problem
A typical Symfony API controller today might look like this:
#[Route('/api/products', methods: ['POST'])]
public function create(
#[MapRequestPayload] CreateProductDto $dto
): JsonResponse {
// business logic
}
This looks clean — and it is.
But when validation fails, Symfony throws a ValidationFailedException.
What you often get out of the box:
- Very verbose error structures
- Debug-oriented responses
- Inconsistent formats depending on where validation happens
{
"type": "https://symfony.com/errors/validation",
"title": "Validation Failed",
"status": 422,
"detail": "name: Name is required\nname: Product name must be at least 3 characters long\nprice: Price must be zero or positive\nstatus: Status must be one of: \"active\", \"inactive\", \"draft\"",
"violations": [
{
"propertyPath": "name",
"title": "Name is required",
"template": "Name is required"
}
],
"trace": ["... stack trace omitted ..."]
}
To fix this, many teams end up:
- Catching exceptions manually
- Writing custom error mappers
- Repeating the same logic in every controller
That’s not ideal.
What We Actually Want
For APIs, validation errors should be:
- JSON-only
- Consistent across all endpoints
- Easy for frontend clients to consume
- Free of Symfony internals
- Automatically handled
{
"errors": {
"name": [
"Product name is required",
"Product name must be at least 3 characters long"
],
"price": [
"Price must be zero or positive"
]
}
}
No try/catch blocks.
No duplicated code.
No controller-level logic.
A Clean Approach
Let validation fail naturally — but intercept the exception once and format it properly.
This is exactly what Symfony Validation Response Bundle does.
It listens for validation failures coming from:
#[MapRequestPayload]#[MapQueryString]#[MapUploadedFile]
And transforms them into clean JSON responses automatically.
Installation
composer require soleinjast/symfony-validation-response
No configuration required to get started.
Example in Action
DTO with Validation
final class CreateProductDto
{
public function __construct(
#[Assert\NotBlank(message: 'Product name is required')]
#[Assert\Length(min: 3)]
public string $name,
#[Assert\PositiveOrZero]
public int $price,
#[Assert\Choice(['active', 'inactive', 'draft'])]
public string $status = 'draft'
) {}
}
Controller
#[Route('/api/products', methods: ['POST'])]
public function create(
#[MapRequestPayload] CreateProductDto $dto
): JsonResponse {
return $this->json(['message' => 'Created'], 201);
}
Invalid Request → Automatic Response
{
"errors": {
"name": [
"Product name is required"
],
"price": [
"This value should be either positive or zero."
],
"status": [
"The value you selected is not a valid choice."
]
}
}
Returned with HTTP 422 (Unprocessable Entity).
Configuration: Validation Response Templates
Create the following file:
config/packages/validation_response.yaml
The bundle ships with two predefined templates:
-
simple(default) -
rfc7807(Problem Details for HTTP APIs)
Simple Format (Default)
validation_response:
format: 'simple'
status_code: 422
{
"errors": {
"email": [
"Invalid email format"
]
}
}
RFC 7807 Format (Problem Details)
validation_response:
format: 'rfc7807'
rfc7807:
type: 'https://api.example.com/errors/validation'
title: 'Validation Failed'
{
"type": "https://api.example.com/errors/validation",
"title": "Validation Failed",
"status": 422,
"detail": "Validation errors detected",
"violations": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
Note: When using
rfc7807, the HTTP status code is always422as defined by the specification.
Defining Your Own Preferred Formatter
If neither simple nor rfc7807 fits your needs, you can define your own custom formatter.
Step 1: Create a Custom Formatter
namespace App\Validation;
use Soleinjast\ValidationResponse\Formatter\FormatterInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
final class ApiValidationFormatter implements FormatterInterface
{
public function format(ConstraintViolationListInterface $violations): array
{
$errors = [];
foreach ($violations as $violation) {
$errors[] = [
'field' => $violation->getPropertyPath(),
'message' => $violation->getMessage(),
'code' => $violation->getCode(),
];
}
return [
'success' => false,
'error_count' => count($errors),
'errors' => $errors,
];
}
}
Registering the Formatter in services.yaml
After creating your formatter, you need to register it and override the bundle’s listener.
# config/services.yaml
services:
# Register your custom formatter
App\Validation\ApiValidationFormatter: ~
# Override the default validation exception listener
Soleinjast\ValidationResponse\EventListener\ValidationExceptionListener:
arguments:
$formatter: '@App\Validation\ApiValidationFormatter'
$statusCode: 422
tags:
- { name: kernel.event_subscriber }
How This Works
- The bundle uses a single event subscriber to intercept validation exceptions
- By redefining it in
services.yaml, Symfony replaces the default formatter - Your formatter is now used everywhere:
- HTTP validation errors
#[MapRequestPayload]#[MapQueryString]#[MapUploadedFile]- CLI validation testing
Bonus: CLI Validation Testing
php bin/console validation:test CreateProductDto '{"name":"","price":-10}'
This command uses the same formatter and configuration as your API.
Design Philosophy
This bundle is intentionally:
- Symfony-native
- Minimal and explicit
- Zero-configuration by default
- Focused on one responsibility only
It doesn’t replace Symfony validation — it simply finishes the job cleanly.
When Should You Use This?
- You build JSON APIs
- You use DTO-based validation
- You want consistent, frontend-friendly errors
- You don’t want validation logic in controllers
Links
- GitHub https://github.com/soleinjast/symfony-validation-response
- Packagist https://packagist.org/packages/soleinjast/symfony-validation-response
Final Thoughts
Validation errors are part of every API — but handling them shouldn’t be painful.
With a small, focused solution, you can keep your controllers clean and your API responses predictable.
Happy coding 🚀
Mohammad Oveisi
Top comments (1)
Useful validation