End-to-end generics-aware OpenAPI clients — unified { data, meta }
responses without boilerplate.
Modern, type-safe OpenAPI client generation — powered by Spring Boot 3.4, Java 21, and OpenAPI Generator 7.16.0.
This repository demonstrates a production-grade setup where backend and client remain fully aligned through generics, supporting nested envelopes like ServiceResponse<Page<T>>
and standardized RFC 9457 — Problem Details for HTTP APIs error handling.
🧠 RFC 9457 vs RFC 7807
RFC 9457 supersedes 7807 and formalizesapplication/problem+json
/application/problem+xml
for HTTP APIs.
Spring Framework 6+ natively supports this via the built-inProblemDetail
class, ensuring consistent error serialization between server and client.
- 📦 Modules
- 🚀 Problem & Motivation
- 💡 Solution Overview
- ⚙️ Architecture Overview
- ⚡ Quick Start
- 🔄 Generated Wrappers — Before & After
- 🧱 Example Responses
- 🧩 Tech Stack
- ✅ Key Features
- ✨ Usage Example
- 📘 Adoption Guides
- 🔗 References & External Links
A clean architecture pattern for building generics-aware OpenAPI clients that stay fully type-safe, consistent, and boilerplate-free.
- customer-service — sample backend exposing
/v3/api-docs.yaml
via Springdoc - customer-service-client — generated OpenAPI client with generics-aware wrappers
OpenAPI Generator, by default, does not handle generic response types.
When backend APIs wrap payloads in ServiceResponse<T>
(e.g., the unified { data, meta }
envelope), the generator produces duplicated models per endpoint instead of a single reusable generic base.
This leads to:
- ❌ Dozens of almost-identical response classes
- ❌ Higher maintenance overhead
- ❌ Harder to evolve a single envelope contract across services
// Default OpenAPI output (before)
public class ServiceResponseCustomerDto {
private CustomerDto data;
private Meta meta;
}
public class ServiceResponsePageCustomerDto {
private PageCustomerDto data; // instead of Page<CustomerDto>
private Meta meta;
}
public class ServiceResponseCustomerDeleteResponse {
private CustomerDeleteResponse data;
private Meta meta;
}
// ... dozens of duplicates
By applying a Mustache overlay, these are replaced by thin wrappers extending a single generic base (ServiceClientResponse<T>
), preserving Page<T>
and ensuring consistent, type-safe API clients.
This project provides a full-stack pattern aligning Spring Boot services and OpenAPI clients through automatic schema introspection and template overlay.
A Springdoc
customizer inspects controller return types such as:
ResponseEntity<ServiceResponse<CustomerDto>>
ResponseEntity<ServiceResponse<Page<CustomerDto>>>
and enriches the generated OpenAPI schema with vendor extensions:
Single type (ServiceResponse<T>
):
x-api-wrapper: true
x-api-wrapper-datatype: CustomerDto
Nested generics (ServiceResponse<Page<T>>
):
x-api-wrapper: true
x-data-container: Page
x-data-item: CustomerDto
These extensions make the OpenAPI spec aware of generic and nested structures — no manual annotations required.
Custom Mustache overlays redefine OpenAPI templates to generate thin, type-safe wrappers extending the reusable base ServiceClientResponse<T>
.
Generated output:
// Single
public class ServiceResponseCustomerDto
extends ServiceClientResponse<CustomerDto> {}
// Paged
public class ServiceResponsePageCustomerDto
extends ServiceClientResponse<Page<CustomerDto>> {}
✅ Supports nested generics like ServiceClientResponse<Page<CustomerDto>>
✅ Automatically maps error responses into RFC 9457 Problem Details
End-to-end generics-aware architecture: from Spring Boot producer to OpenAPI client consumer.
Layer | Description |
---|---|
Server (Producer) | Publishes an OpenAPI 3.1-compliant spec via Springdoc 2.8.13 with auto-registered wrapper schemas |
Client (Consumer) | Uses OpenAPI Generator 7.16.0 with Mustache overlays for generics support |
Envelope Model | Unified { data, meta } response structure |
Error Handling | RFC 9457-compliant Problem Details decoded into ClientProblemException |
Nested Generics | Full support for ServiceResponse<Page<T>> |
# Run backend service
cd customer-service && mvn spring-boot:run
# Generate and build client
cd ../customer-service-client && mvn clean install
Generated wrappers appear under:
target/generated-sources/openapi/src/gen/java
Each wrapper extends ServiceClientResponse<T>
and aligns with the unified { data, meta }
envelope.
Now you can test end-to-end type-safe responses via the generated client — validating both single and paged envelopes.
Before (duplicated full models):
Each endpoint generated its own response class, duplicating data
and meta
fields.
After (thin generic wrapper):
Each endpoint now extends the reusable ServiceClientResponse<Page<T>>
base, eliminating boilerplate and preserving type safety.
Unified envelope structure applies to both single and paged results.
{
"data": {
"customerId": 1,
"name": "Jane Doe",
"email": "[email protected]"
},
"meta": {
"serverTime": "2025-01-01T12:34:56Z",
"sort": []
}
}
{
"data": {
"content": [
{ "customerId": 1, "name": "Jane Doe", "email": "[email protected]" },
{ "customerId": 2, "name": "John Smith", "email": "[email protected]" }
],
"page": 0,
"size": 5,
"totalElements": 37,
"totalPages": 8,
"hasNext": true,
"hasPrev": false
},
"meta": {
"serverTime": "2025-01-01T12:34:56Z",
"sort": [ { "field": "CUSTOMER_ID", "direction": "ASC" } ]
}
}
Content-Type:
application/json
(success) Content-Type:application/problem+json
(error — RFC 9457)
ServiceClientResponse<Page<CustomerDto>> resp =
customerClientAdapter.getCustomers("Jane", null, 0, 5, SortField.CUSTOMER_ID, SortDirection.ASC);
Page<CustomerDto> page = resp.getData();
for (CustomerDto c : page.content()) {
// ...
}
Component | Version | Purpose |
---|---|---|
Java | 21 | Language baseline |
Spring Boot | 3.4.10 | REST + OpenAPI provider |
Springdoc | 2.8.13 | OpenAPI 3.1 integration |
OpenAPI Generator | 7.16.0 | Generics-aware code generation |
HttpClient5 | 5.5 | Pooled, production-ready HTTP backend |
- 🔹 Unified
{ data, meta }
response model - 🔹 Nested generics support —
ServiceResponse<Page<T>>
- 🔹 RFC 9457-compliant Problem Details (
application/problem+json
) - 🔹 Mustache overlays for thin wrapper generation
- 🔹 Full alignment between producer and consumer
- 🔹 Zero boilerplate — clean, evolvable, and type-safe
public interface CustomerClientAdapter {
ServiceClientResponse<CustomerDto> createCustomer(CustomerCreateRequest request);
ServiceClientResponse<CustomerDto> getCustomer(Integer customerId);
ServiceClientResponse<Page<CustomerDto>> getCustomers();
}
A stable adapter contract hides generated artifacts while preserving strong typing and client independence.
See integration details under docs/adoption
:
- 🌐 GitHub Repository
- 📰 Medium — We Made OpenAPI Generator Think in Generics
- 📘 RFC 9457 — Problem Details for HTTP APIs
Licensed under MIT — see LICENSE.
If you spot an error or have suggestions, open an issue or join the discussion — contributions are welcome. 💭 Start a discussion →
Using this pattern in your project?
Share your experience or ideas in GitHub Discussions — even short notes help others learn and improve the pattern.
💡 Real-world usage feedback helps evolve the templates and guides faster.
Contributions, issues, and feature requests are welcome! Feel free to open an issue or submit a PR.
If you found this project helpful, please give it a ⭐ on GitHub — it helps others discover it.
Barış Saylı GitHub · Medium · LinkedIn
📖 This repository accompanies the article
We Made OpenAPI Generator Think in Generics
published on Medium, which serves as the canonical write-up for this implementation.