OpenAPI Generics β Keep Your API Contract Intact End-to-End
Define your API once in Java.
Preserve it across OpenAPI and generated clients β without duplication or drift.
Table of Contents
- Why this exists
- What you actually do
- β‘ Quick Start
- Result
- Proof β Generated Client (Before vs After)
- What changed
- What is actually generated
- How you actually use it
- What this gives you
- Why this matters
- Mental model
- Next steps
- π References & External Links
- Final note
Why this exists
In most OpenAPI-based workflows:
- generics are flattened or lost
- response envelopes are regenerated per endpoint
- clients gradually drift from server-side contracts
Over time, this creates a gap between what your API defines and what your clients consume.
The result is not an immediate failure β but a slow erosion of contract integrity.
This platform removes that entire class of problems.
Your Java contract remains the single source of truth β across all layers.
What you actually do
You donβt configure OpenAPI.
You donβt maintain templates.
You donβt fight generator behavior.
You only do two things:
- return your contract from controllers
- generate clients from OpenAPI
Thatβs it.
The platform handles projection, generation, and contract alignment automatically.
β‘ Quick Start
1. Server (producer)
Add the dependency:
<dependency>
<groupId>io.github.blueprint-platform</groupId>
<artifactId>openapi-generics-server-starter</artifactId>
<version>0.8.1</version>
</dependency>
Return your contract:
ServiceResponse<CustomerDto>
2. Client (consumer)
Inherit the parent:
<parent>
<groupId>io.github.blueprint-platform</groupId>
<artifactId>openapi-generics-java-codegen-parent</artifactId>
<version>0.8.1</version>
</parent>
Generate the client:
mvn clean install
Result
ServiceResponse<CustomerDto>
The exact same contract type flows from server to client.
- no duplicated models
- generics preserved end-to-end
- contract types reused (not regenerated)
Proof β Generated Client (Before vs After)
Before (default OpenAPI behavior)
- duplicated envelope per endpoint
- generics flattened or lost
- unstable and verbose model graph
After (contract-aligned generation)
public class ServiceResponsePageCustomerDto
extends ServiceResponse<Page<CustomerDto>> {}
- no envelope duplication
- generics preserved end-to-end
- contract types reused directly
What changed
Instead of generating new models from OpenAPI:
- the contract is preserved
- wrappers are generated as thin type bindings
- the client reuses existing domain semantics
Result:
Java Contract (SSOT)
β
OpenAPI (projection, not authority)
β
Generator (deterministic reconstruction)
β
Client (canonical contract types)
No reinterpretation. No duplication. No drift.
What is actually generated
The client does not recreate your models.
Instead, it generates thin wrapper classes that bind OpenAPI responses back to your canonical contract.
Example:
public class ServiceResponseCustomerDto extends ServiceResponse<CustomerDto> {
}
public class ServiceResponsePageCustomerDto extends ServiceResponse<Page<CustomerDto>> {
}
Key properties:
- no envelope duplication
- no structural redefinition
- no generic type loss
These classes exist only to bridge OpenAPI β Java type system.
How you actually use it
You never interact with generated wrappers directly.
Instead, you define an adapter layer:
public interface CustomerClientAdapter {
ServiceResponse<CustomerDto> createCustomer(CustomerCreateRequest request);
ServiceResponse<CustomerDto> getCustomer(Integer customerId);
ServiceResponse<Page<CustomerDto>> getCustomers();
}
Implementation delegates to generated API:
@Service
public class CustomerClientAdapterImpl implements CustomerClientAdapter {
private final CustomerControllerApi api;
public CustomerClientAdapterImpl(CustomerControllerApi api) {
this.api = api;
}
@Override
public ServiceResponse<CustomerDto> getCustomer(Integer customerId) {
return api.getCustomer(customerId);
}
@Override
public ServiceResponse<Page<CustomerDto>> getCustomers() {
return api.getCustomers(null, null, 0, 5, "customerId", "ASC");
}
}
What this gives you
At usage level, your application only sees:
ServiceResponse<CustomerDto>
ServiceResponse<Page<CustomerDto>>
Not:
- generated wrapper classes
- duplicated DTO hierarchies
- OpenAPI-specific models
No translation layer. No reinterpretation. No drift.
Why this matters
Traditional OpenAPI generation produces:
- duplicated response envelopes
- flattened generics
- unstable model graphs
This approach guarantees:
- a single contract shared across all layers
- stable and predictable client generation
- zero drift between server and client semantics
Mental model
Think of generated classes as:
thin type adapters β not models
They exist because OpenAPI cannot express Java generics β not because your model requires them.
They simply reconnect OpenAPI output back to your existing contract, without redefining it.
Your system always operates on:
ServiceResponse<T>
Everything else is just infrastructure.
Next steps
π References & External Links
- π GitHub Repository β openapi-generics
- π Medium β We Made OpenAPI Generator Think in Generics
Final note
If the contract stays consistent, everything stays consistent.
This system works by keeping that boundary intact.