DEV Community

Cover image for I had to build my own Symfony validation bundle because no existing one fits my requirements.
David Evdoshchenko
David Evdoshchenko

Posted on

I had to build my own Symfony validation bundle because no existing one fits my requirements.

It all started four years ago with a Symfony API that wasn't supposed to be anything special. Why does it always start like that?

Back then, in the Symfony 3.* era, no one had big plans for this service. Fast forward two years, and it had morphed into the company's primary data gateway, ha-ha. Classic.

At first, we tackled tasks as they came. The app learned to validate incoming data, but as requests grew more complex, I found myself drowning in endless checks: "If there's a User, the Email is mandatory - but only for web requests. If it's mobile, we need a specific header with a valid value..." You know the drill.

Trying to stick to "Symfony Best Practices" led to a nightmare of asserts and DTO layers. I needed custom serializers, normalizers, the works. Eventually, I had more validation code than actual business logic. I was literally lost in my own codebase.

I looked at API Platform and thought: "This is it, the silver bullet!" But...

To the folks presenting at big conferences: do you actually live in the real world? Or is your entire universe just an idealized SOLID landscape where every edge case fits a perfect pattern? API Platform expects the world to play by its rules - everything mapped, everything linked, everything strictly typed. That's great for a "Hello World" project.

Don't get me wrong - I'm well aware of architectural principles, layers, and all the usual "bullshit." But I've developed my own perspective on these things: an understanding of the compromises between "doing it right" according to a textbook and "doing what's actually needed" to ship a product.

In my reality, no one cared about entity relationships, and data types were often a mystery until the moment they hit the server. It's like socks: a sock only becomes "left" when you put it on your left foot.

I kept searching and stumbled upon JSON Schema. It's not just a library; it's a mindset. The spec is so flexible that I've yet to find a requirement I couldn't formalize. Life got easier instantly: one concise schema file per route, and my controllers "slimmed down" overnight.

But the documentation gap remained. Writing it manually meant a permanent desync. Generating it from PHP 8 attributes? Still risky - an attribute might say one thing while the JSON Schema in the code does another.

Then came the OpenAPI 3.0 spec, which is fully compatible with JSON Schema. That's when it clicked: what if I could inject the JSON Schema directly into the generated API specification?

That's how this bundle was born.

The Problem it Solves

It makes JSON Schema your Single Source of Truth, eliminating "lying" documentation:

  • Real Validation: Validates requests using a proper JSON Schema engine.
  • Automatic DTOs: Maps data to DTOs (or returns an object if you don't need a DTO).
  • Instant Docs: Automatically pushes the schema into Nelmio/Swagger.

Show me the code
Here is how simple a controller looks when you stop fighting with complex abstractions:

#[OA\Post(
    operationId: 'createUserWithDocs',
    summary: 'Documented endpoint',
    responses: [
        new OA\Response(
            response: 200,
            description: 'User created successfully',
            content: new OA\JsonContent(ref: new Model(type: UserApiDtoResponse::class))
        )
    ]
)]
#[Route('/api-user', methods: ['POST'])]
public function createUser(
    #[MapRequest('schemas/user-create.json')] UserApiDtoRequest $user
): JsonResponse {
    // 1. The JSON body is already validated against 'user-create.json'
    // 2. The $user DTO is already populated
    // 3. The OpenAPI spec is automatically updated with your JSON Schema

    $userData = UserApiDtoResponse::fromArray([
        'name'  => $user->name,
        'email' => $user->email,
        'age'   => $user->age,
    ]);

    return $this->json($userData, 200);
}
Enter fullscreen mode Exit fullscreen mode

The Result
You change one JSON file, and both your validation and your documentation update simultaneously. No more lying to the frontend team.

The unexpected win: Since API responses are now strictly based on model, you can move these definitions to a shared repository. The frontend team can then reuse them to deserialize API responses directly. It ensures total consistency across the board—no more "undefined" errors because of a typo in a key.

If you want to check out the implementation or test it in your project, grab it here:

Feedback and GitHub stars are much appreciated! ⭐

P.S. If your team needs someone who prioritizes working solutions over infinite abstractions - and knows exactly which foot the "project sock" goes on - I'm currently open to new opportunities. Let's talk! 🤝

Top comments (0)