<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohammad Oveisi</title>
    <description>The latest articles on DEV Community by Mohammad Oveisi (@mohammad_oveisi_9625d74d1).</description>
    <link>https://dev.to/mohammad_oveisi_9625d74d1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3134329%2F9d432ed9-9c25-4c48-9488-69b9eea062ff.jpg</url>
      <title>DEV Community: Mohammad Oveisi</title>
      <link>https://dev.to/mohammad_oveisi_9625d74d1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohammad_oveisi_9625d74d1"/>
    <language>en</language>
    <item>
      <title>Serve AI-Ready Markdown from Your Symfony App</title>
      <dc:creator>Mohammad Oveisi</dc:creator>
      <pubDate>Sat, 21 Feb 2026 13:07:54 +0000</pubDate>
      <link>https://dev.to/mohammad_oveisi_9625d74d1/serve-ai-ready-markdown-from-your-symfony-app-162m</link>
      <guid>https://dev.to/mohammad_oveisi_9625d74d1/serve-ai-ready-markdown-from-your-symfony-app-162m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;AI agents don't read your website the way humans do. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;They don't care about your Tailwind or any CSS classes!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They ignore your navigation menus!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They don't execute your JavaScript!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They just want &lt;strong&gt;clean, structured content&lt;/strong&gt;. As tools like ChatGPT, Claude, Perplexity, and autonomous crawlers increasingly browse the web, serving raw HTML to them becomes inefficient. HTML contains layout noise --- headers, footers, scripts, styles, cookie banners --- all of which waste tokens and reduce semantic clarity.&lt;/p&gt;

&lt;p&gt;What AI systems prefer is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Clean structure!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Clear headings!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Minimal markup!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Content without presentation noise!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of duplicating templates or maintaining a second content pipeline, I built a solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing &lt;code&gt;symfony-markdown-response-bundle&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;👉 GitHub: &lt;a href="https://github.com/soleinjast/symfony-markdown-response-bundle" rel="noopener noreferrer"&gt;https://github.com/soleinjast/symfony-markdown-response-bundle&lt;/a&gt;&lt;br&gt;&lt;br&gt;
📦 Packagist: &lt;a href="https://packagist.org/packages/soleinjast/symfony-markdown-response-bundle" rel="noopener noreferrer"&gt;https://packagist.org/packages/soleinjast/symfony-markdown-response-bundle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lightweight Symfony bundle that transparently converts your existing HTML responses into Markdown — only when requested.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No duplicate routes!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No separate Markdown controllers!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No extra maintenance!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just AI-ready Symfony responses.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;For 20+ years, we optimized the web for browsers.&lt;/p&gt;

&lt;p&gt;Now we also need to optimize it for &lt;strong&gt;machines that read and reason&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When an AI agent visits your site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It consumes tokens.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It processes structure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It extracts meaning.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Serving full HTML with layout wrappers increases token usage and reduces signal quality.&lt;/p&gt;

&lt;p&gt;Markdown, on the other hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Preserves headings and hierarchy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keeps paragraphs intact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Removes presentation noise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improves semantic clarity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is dramatically lighter.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What This Bundle Does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;symfony-markdown-response-bundle&lt;/code&gt; intercepts Symfony responses and, when appropriate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Detects whether the request prefers Markdown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cleans the HTML output.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Converts it to Markdown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Returns &lt;code&gt;text/markdown&lt;/code&gt; instead of &lt;code&gt;text/html&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All transparently.&lt;/p&gt;

&lt;p&gt;Your controllers remain unchanged.&lt;/p&gt;


&lt;h2&gt;
  
  
  Conversion Drivers
&lt;/h2&gt;

&lt;p&gt;The bundle supports two conversion backends.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;local&lt;/code&gt; (default)
&lt;/h3&gt;

&lt;p&gt;Uses &lt;code&gt;league/html-to-markdown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conversion happens in-process with no external dependencies.&lt;br&gt;
Before conversion, the following nodes are stripped automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;head&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;script&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;style&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;nav&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;footer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;aside&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  &lt;code&gt;cloudflare&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;You can offload conversion to a Cloudflare Workers AI endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;symfony_markdown_response&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare&lt;/span&gt;
    &lt;span class="na"&gt;cloudflare_endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://your-worker.example.workers.dev/to-markdown'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires &lt;code&gt;symfony/http-client&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The HTML is posted to your Worker endpoint, and the response body is returned as Markdown.&lt;/p&gt;




&lt;h2&gt;
  
  
  Smart Caching
&lt;/h2&gt;

&lt;p&gt;Converted Markdown is cached by default using a PSR-6 cache pool.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Cache key: &lt;code&gt;xxh3&lt;/code&gt; hash of the preprocessed HTML&lt;/li&gt;
&lt;li&gt;  Default TTL: 3600 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cache resolution order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;code&gt;cache_service&lt;/code&gt; (if configured)&lt;/li&gt;
&lt;li&gt; &lt;code&gt;cache.app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; &lt;code&gt;cache.system&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; No caching&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To disable caching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;symfony_markdown_response&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cache_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1️⃣ Opt-In via Attribute
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Soleinjast\MarkdownResponseBundle\Attribute\ProvideMarkdownResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Route('/about')]&lt;/span&gt;
&lt;span class="na"&gt;#[ProvideMarkdownResponse(enabled: true)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;about&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'about.html.twig'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2️⃣ Smart Request Detection
&lt;/h3&gt;

&lt;p&gt;The bundle determines whether to serve Markdown based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;Accept: text/markdown&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;.md&lt;/code&gt; URL suffix (e.g. &lt;code&gt;/about.md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;  Known AI user agents (GPTBot, ClaudeBot, ChatGPT-User, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Normal browser traffic continues receiving HTML.&lt;br&gt;
AI agents get Markdown automatically.&lt;/p&gt;




&lt;h3&gt;
  
  
  3️⃣ Clean Conversion Pipeline
&lt;/h3&gt;

&lt;p&gt;Before conversion, the bundle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Removes layout noise&lt;/li&gt;
&lt;li&gt;  Keeps semantic content intact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it converts the cleaned HTML into structured Markdown.&lt;/p&gt;

&lt;p&gt;Optional caching ensures performance remains optimal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require soleinjast/symfony-markdown-response-bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then enable it in your Symfony project and annotate your routes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The web is no longer consumed only by humans.&lt;/p&gt;

&lt;p&gt;If your application already produces meaningful HTML, why not let machines consume it in the format they understand best?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;symfony-markdown-response-bundle&lt;/code&gt; bridges that gap --- cleanly,&lt;br&gt;
transparently, and with zero duplication.&lt;/p&gt;

</description>
      <category>php</category>
      <category>symfony</category>
      <category>markdown</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Handle Validation Errors in Symfony the Right Way</title>
      <dc:creator>Mohammad Oveisi</dc:creator>
      <pubDate>Thu, 01 Jan 2026 13:31:27 +0000</pubDate>
      <link>https://dev.to/mohammad_oveisi_9625d74d1/how-to-handle-validation-errors-in-symfony-the-right-way-3a4f</link>
      <guid>https://dev.to/mohammad_oveisi_9625d74d1/how-to-handle-validation-errors-in-symfony-the-right-way-3a4f</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Handling validation errors is something every Symfony API needs — yet it often ends up &lt;strong&gt;messy, repetitive, and inconsistent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Modern Symfony makes request validation elegant with DTOs and attributes like &lt;code&gt;#[MapRequestPayload]&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
But when validation fails, the &lt;strong&gt;default error-handling story still isn’t great&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this post, I’ll show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The common problem with validation errors in Symfony APIs&lt;/li&gt;
&lt;li&gt;Why the default behavior doesn’t scale well&lt;/li&gt;
&lt;li&gt;A clean, modern approach to solving it&lt;/li&gt;
&lt;li&gt;How to get consistent JSON validation responses with &lt;strong&gt;zero boilerplate&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The Common Problem
&lt;/h2&gt;

&lt;p&gt;A typical Symfony API controller today might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="na"&gt;#[Route('/api/products', methods: ['POST'])]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;MapRequestPayload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;CreateProductDto&lt;/span&gt; &lt;span class="nv"&gt;$dto&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// business logic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks clean — and it is.&lt;/p&gt;

&lt;p&gt;But when validation fails, Symfony throws a &lt;code&gt;ValidationFailedException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What you often get out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very verbose error structures&lt;/li&gt;
&lt;li&gt;Debug-oriented responses&lt;/li&gt;
&lt;li&gt;Inconsistent formats depending on where validation happens
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://symfony.com/errors/validation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Validation Failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name: Name is required&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;name: Product name must be at least 3 characters long&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;price: Price must be zero or positive&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;status: Status must be one of: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;inactive&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"violations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"propertyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name is required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name is required"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"... stack trace omitted ..."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To fix this, many teams end up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Catching exceptions manually&lt;/li&gt;
&lt;li&gt;Writing custom error mappers&lt;/li&gt;
&lt;li&gt;Repeating the same logic in every controller&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not ideal.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Actually Want
&lt;/h2&gt;

&lt;p&gt;For APIs, validation errors should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON-only&lt;/li&gt;
&lt;li&gt;Consistent across all endpoints&lt;/li&gt;
&lt;li&gt;Easy for frontend clients to consume&lt;/li&gt;
&lt;li&gt;Free of Symfony internals&lt;/li&gt;
&lt;li&gt;Automatically handled
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Product name is required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Product name must be at least 3 characters long"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Price must be zero or positive"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No try/catch blocks.&lt;br&gt;&lt;br&gt;
No duplicated code.&lt;br&gt;&lt;br&gt;
No controller-level logic.&lt;/p&gt;


&lt;h2&gt;
  
  
  A Clean Approach
&lt;/h2&gt;

&lt;p&gt;Let validation fail naturally — but intercept the exception once and format it properly.&lt;/p&gt;

&lt;p&gt;This is exactly what &lt;strong&gt;Symfony Validation Response Bundle&lt;/strong&gt; does.&lt;/p&gt;

&lt;p&gt;It listens for validation failures coming from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#[MapRequestPayload]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[MapQueryString]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[MapUploadedFile]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And transforms them into clean JSON responses automatically.&lt;/p&gt;


&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require soleinjast/symfony-validation-response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No configuration required to get started.&lt;/p&gt;


&lt;h2&gt;
  
  
  Example in Action
&lt;/h2&gt;
&lt;h3&gt;
  
  
  DTO with Validation
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProductDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;#[Assert\NotBlank(message: 'Product name is required')]&lt;/span&gt;
        &lt;span class="na"&gt;#[Assert\Length(min: 3)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="na"&gt;#[Assert\PositiveOrZero]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="na"&gt;#[Assert\Choice(['active', 'inactive', 'draft'])]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Controller
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="na"&gt;#[Route('/api/products', methods: ['POST'])]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;MapRequestPayload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;CreateProductDto&lt;/span&gt; &lt;span class="nv"&gt;$dto&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;JsonResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Created'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Invalid Request → Automatic Response
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Product name is required"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"This value should be either positive or zero."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"The value you selected is not a valid choice."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Returned with &lt;strong&gt;HTTP 422 (Unprocessable Entity)&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Configuration: Validation Response Templates
&lt;/h2&gt;

&lt;p&gt;Create the following file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config/packages/validation_response.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bundle ships with &lt;strong&gt;two predefined templates&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;simple&lt;/code&gt; (default)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rfc7807&lt;/code&gt; (Problem Details for HTTP APIs)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Simple Format (Default)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;validation_response&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;simple'&lt;/span&gt;
    &lt;span class="na"&gt;status_code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;422&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Invalid email format"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  RFC 7807 Format (Problem Details)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;validation_response&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rfc7807'&lt;/span&gt;
    &lt;span class="na"&gt;rfc7807&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/errors/validation'&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Validation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Failed'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com/errors/validation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Validation Failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Validation errors detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"violations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid email format"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When using &lt;code&gt;rfc7807&lt;/code&gt;, the HTTP status code is always &lt;code&gt;422&lt;/code&gt; as defined by the specification.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Defining Your Own Preferred Formatter
&lt;/h2&gt;

&lt;p&gt;If neither &lt;code&gt;simple&lt;/code&gt; nor &lt;code&gt;rfc7807&lt;/code&gt; fits your needs, you can define &lt;strong&gt;your own custom formatter&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a Custom Formatter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Validation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Soleinjast\ValidationResponse\Formatter\FormatterInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\ConstraintViolationListInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiValidationFormatter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;FormatterInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ConstraintViolationListInterface&lt;/span&gt; &lt;span class="nv"&gt;$violations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$violations&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$violation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'field'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$violation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPropertyPath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$violation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="s1"&gt;'code'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$violation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'error_count'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'errors'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Registering the Formatter in services.yaml
&lt;/h2&gt;

&lt;p&gt;After creating your formatter, you need to register it and override the bundle’s listener.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/services.yaml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Register your custom formatter&lt;/span&gt;
    &lt;span class="na"&gt;App\Validation\ApiValidationFormatter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~&lt;/span&gt;

    &lt;span class="c1"&gt;# Override the default validation exception listener&lt;/span&gt;
    &lt;span class="na"&gt;Soleinjast\ValidationResponse\EventListener\ValidationExceptionListener&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;$formatter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@App\Validation\ApiValidationFormatter'&lt;/span&gt;
            &lt;span class="na"&gt;$statusCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;422&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;kernel.event_subscriber&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How This Works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The bundle uses a single event subscriber to intercept validation exceptions&lt;/li&gt;
&lt;li&gt;By redefining it in &lt;code&gt;services.yaml&lt;/code&gt;, Symfony replaces the default formatter&lt;/li&gt;
&lt;li&gt;Your formatter is now used everywhere:

&lt;ul&gt;
&lt;li&gt;HTTP validation errors&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[MapRequestPayload]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[MapQueryString]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#[MapUploadedFile]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CLI validation testing&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bonus: CLI Validation Testing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php bin/console validation:test CreateProductDto &lt;span class="s1"&gt;'{"name":"","price":-10}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command uses the &lt;strong&gt;same formatter and configuration&lt;/strong&gt; as your API.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Philosophy
&lt;/h2&gt;

&lt;p&gt;This bundle is intentionally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Symfony-native&lt;/li&gt;
&lt;li&gt;Minimal and explicit&lt;/li&gt;
&lt;li&gt;Zero-configuration by default&lt;/li&gt;
&lt;li&gt;Focused on one responsibility only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It doesn’t replace Symfony validation — it simply &lt;strong&gt;finishes the job cleanly&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Should You Use This?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You build JSON APIs&lt;/li&gt;
&lt;li&gt;You use DTO-based validation&lt;/li&gt;
&lt;li&gt;You want consistent, frontend-friendly errors&lt;/li&gt;
&lt;li&gt;You don’t want validation logic in controllers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub
&lt;a href="https://github.com/soleinjast/symfony-validation-response" rel="noopener noreferrer"&gt;https://github.com/soleinjast/symfony-validation-response&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Packagist
&lt;a href="https://packagist.org/packages/soleinjast/symfony-validation-response" rel="noopener noreferrer"&gt;https://packagist.org/packages/soleinjast/symfony-validation-response&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Validation errors are part of every API — but handling them shouldn’t be painful.&lt;/p&gt;

&lt;p&gt;With a small, focused solution, you can keep your controllers clean and your API responses predictable.&lt;/p&gt;

&lt;p&gt;Happy coding 🚀&lt;br&gt;&lt;br&gt;
Mohammad Oveisi&lt;/p&gt;

</description>
      <category>php</category>
      <category>symfony</category>
      <category>api</category>
      <category>validation</category>
    </item>
  </channel>
</rss>
