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 (6)
Great read! Handling validation errors properly in Symfony is crucial for both UX and data integrity. I like how you emphasized using the Validator component and custom error messages—it really keeps the code clean and user-friendly. Also, integrating error handling with forms and API responses makes a huge difference in maintainability.
Why would someone prefer an extra library over a application specific solution?
The library is basically an opinionated custom solution. So why would the opinion of the library be so universal that it fulfills the needs of most applications?
The red flag for me is that you added a trace key to the default output. The smart people of Symfony aren't going to expose the stack trace because it is a security risk knowing that information.
That the output will not be useful for everyone, is because it is a framework. They provide a sensible response that is easy to override.
Thanks for the feedback — a couple of clarifications might help.
The goal of the library isn’t to replace application-specific logic, but to handle a cross-cutting concern consistently: formatting validation errors for JSON APIs. This is similar to why we use Monolog, serializers, or middleware instead of re-implementing those concerns per project.
Regarding being “opinionated”: the bundle provides sensible defaults, but everything is fully overridable via standard Symfony service overriding.
About the trace key — that output comes from Symfony’s default debug exception handling, not from this library. In fact, one of the motivations for the bundle is to avoid exposing debug-oriented structures (including stack traces) in API responses and return clean, predictable JSON instead.
If an application prefers a bespoke solution, that’s totally valid. This library is simply an option for teams that want consistency without repeating the same glue code across projects.
To be clear I'm not saying it is a bad library. The point I'm trying to make is that with the flexibility of the framework adding an extra library is not needed, even if you want a reusable solution.
Read my comment again. I never linked the stack trace with the library
Isn't all custom code glue code? Even the library.
This sentence feels like you know better than all the other developers that use Symfony.
Thanks for clarifying — I think we’re actually closer in opinion than it might look.
I agree that Symfony is flexible enough to solve this at the application level, and for many teams that’s absolutely the right choice. The library isn’t meant to suggest otherwise, nor to replace application-specific decisions.
The motivation here is simply reuse and consistency for teams that already find themselves solving the same validation-response problem across multiple projects. In that context, extracting the solution into a small, focused bundle can be a pragmatic trade-off — not a claim that this is “the” correct way.
On the “glue code” point: you’re right in a broad sense — everything is glue at some level. The distinction I’m trying to make is between duplicated, per-project glue and a shared, explicit abstraction that teams can choose to adopt or ignore.
If a team prefers keeping this logic inline or bespoke per application, that’s completely valid. This library is just one option for those who value a reusable, centralized approach.
Useful validation