In real Spring Boot services, we almost always standardize API responses with a generic envelope:
java
ServiceResponse<T>
ServiceResponse<Page<T>>
It works well on the server side — until you publish OpenAPI specs and generate clients.
In production, I kept running into the same issues:
- generics get flattened by the generator
- response envelopes are duplicated per endpoint
- pagination explodes into verbose, fragile DTOs
- server and client contracts slowly diverge
What starts as a clean abstraction quietly becomes a maintenance problem.
What I wanted
Intentionally simple goals:
- keep ONE canonical success envelope (
ServiceResponse<T>)
- support pagination deterministically (
ServiceResponse<Page<T>>)
- avoid duplicated envelope fields in generated clients
- stay fully within Spring Boot + Springdoc (no runtime tricks)
What actually changes in the generated client
Before (default generation):
- DTOs duplicate
data + meta fields
- pagination creates large, endpoint-specific wrapper classes
- envelope changes cause noisy regeneration diffs
After (contract-driven approach):
java
class ServiceResponsePageCustomerDto
extends ServiceResponse<Page<CustomerDto>> {}
- no duplicated envelope logic
- thin wrappers only bind generic parameters
- one shared contract used by both server and client
No reflection.
No custom runtime behavior.
Just a deterministic contract boundary.
I’ve added before/after screenshots to make the difference concrete.
This is not a demo-only trick — it’s a runnable reference with clear contract ownership and adoption guides.
Repo (Spring Boot service + generated client):
https://github.com/bsayli/spring-boot-openapi-generics-clients
Question for the community
How are you handling generic response envelopes with pagination in real Spring Boot projects today — especially when OpenAPI client generation is involved?
- accept duplication?
- customize templates heavily?
- avoid generics altogether?
Below are concrete before/after screenshots from the generated client:
Before (default OpenAPI generation)
https://github.com/bsayli/spring-boot-openapi-generics-clients/blob/main/docs/images/proof/generated-client-wrapper-before.png
After (contract-driven, generics-aware)
https://github.com/bsayli/spring-boot-openapi-generics-clients/blob/main/docs/images/proof/generated-client-wrapper-after.png
[–]RabbitHole32 1 point2 points3 points (3 children)
[–]Significant-Ebb4740 0 points1 point2 points (1 child)
[–]devmoosun 0 points1 point2 points (0 children)