For years, I followed what most Symfony developers would call best practices. A new API endpoint appeared in the backlog, and the process was always the same:
- Design the domain model
- Create DTOs
- Add validation constraints
- Connect everything together
It looked clean and professional. It looked exactly like the examples I saw in conference talks, blog posts, and framework documentation.
And it almost never survived contact with reality.
Not because Symfony was wrong, not because DTOs were bad, but because the projects I worked on had one thing in common: the business didn't actually know its data model yet.
The Assumption Behind Most API Design
Most API examples start with a stable domain:
- a customer has a name.
- an order has a status.
- a product has a price.
From there, everything makes sense. You build entities and DTOs. Then attach validation rules, expose an API.
The model exists first - the API is built around it. This works beautifully when the model is known.
But many enterprise projects don't work like that.
The Reality I Kept Seeing
For several years, I worked on APIs inside a large company. The company was not selling software, software was simply one of the tools supporting the real business. That changes everything. Because the business rarely arrives with a complete model. Instead, requirements often look like this:
We need to accept these fields.
A few weeks later:
We also need these additional fields.
A month later:
Another partner sends a slightly different structure.
And eventually:
Existing clients must continue working exactly as before.
At that point, the beautiful DTO hierarchy starts fighting reality. Not because the code is bad. Because the assumptions were wrong. We assumed we already understood the shape of the domain.
We didn't.
The Endless Refactoring Cycle
The pattern became familiar.
Week 1:
{
"customerId": "123",
"amount": 100
}
Week 3:
{
"customerId": "123",
"amount": 100,
"customerType": "business"
}
Week 6:
{
"customerId": "123",
"amount": 100,
"customerType": "business",
"partnerSpecificData": {}
}
Week 10:
{
"customerId": "123",
"amount": 100,
"legacyField": "must still be accepted"
}
Every change triggered another round of:
- DTO updates
- Validator updates
- Serializer changes
- Documentation changes
The code stayed clean, the model stayed elegant, the API contract remained unstable.
The Moment I Changed My Mind
At some point I realized I was solving the wrong problem. I wasn't trying to model the business. I was trying to define what the API accepted. Those are not always the same thing. An API contract answers a much simpler question:
What payload is valid today?
It doesn't need to predict the future, to represent the perfect domain model. It only needs to describe the contract between a client and a server. That realization pushed me toward JSON Schema.
Why JSON Schema Felt Different
JSON Schema starts from a completely different assumption. It doesn't care about entities, DTOs and about object hierarchies. It only describes data.
For example:
{
"type": "object",
"required": ["customerId"],
"properties": {
"customerId": {
"type": "string"
},
"amount": {
"type": "number"
}
}
}
That's it. A contract - nothing more, nothing less. The schema says what is allowed. The application decides what to do with it.
Contract First, Model Later
The biggest shift for me was psychological. I stopped pretending I already knew the final model. Instead, I accepted that the contract would evolve. The schema could evolve with it. When the business changed requirements, I updated the contract.
- not an entire object graph.
- not a DTO hierarchy.
- not a collection of serializer groups.
Just the contract.
Over time, stable patterns emerged. Only then did it make sense to extract real domain concepts.
This Is Why I Built My Symfony Bundle
Eventually I wanted to use JSON Schema validation directly inside Symfony applications. Not as documentation or as generated code.
As a runtime contract.
I wanted to take a schema, validate incoming payloads, and know immediately whether the request matched the agreement between the client and the server. That idea eventually became the bundle I've been working on. But the bundle itself is not the important part. The important part is the lesson that led to it.
Final Thoughts
I still use DTOs. I still use validation constraints. I still think they are excellent tools. But I no longer start with them.
When requirements are unstable and the business is still discovering its own processes, I start with the contract. For me, that contract is often a JSON Schema.
Not because it's more elegant - because it reflects reality better.
p.s. and I strongly suspect that 95% of businesses in the world operate under this kind of entropy, and some of the readers here are either in it right now or have experienced it before. Tell me how you deal with it — how do you fight this entropy?
Top comments (0)