- Why this exists (practical impact)
- Key features in 1.0.0 (GA)
- Real usage (what you actually do)
- Quick start (2 minutes)
- Compatibility
- Contract lifecycle model
- Core idea
- System Architecture Overview
- Proof — before vs after
- Design guarantees
- Modules
- References
- Contributing
- License
In most OpenAPI-based systems:
- generics are flattened or lost
- response envelopes are regenerated per endpoint
- client models drift from server contracts over time
This creates hidden long-term cost:
- duplicated DTO hierarchies
- fragile client regeneration
- broken assumptions across services
This project removes that entire class of problems.
Define your contract once in Java — reuse it everywhere without drift.
This is no longer a template-level customization.
It is now a contract-aligned generation system with progressive adoption — designed to adapt to existing architectures instead of forcing new ones.
Use your existing response envelope without migrating to ServiceResponse.
<additionalProperties>
<additionalProperty>
openapi-generics.envelope=io.example.contract.ApiResponse
</additionalProperty>
</additionalProperties>Result:
- no forced migration to a new envelope type
- your response model remains intact
- existing contracts continue to work as-is
Behavior:
- If not configured →
ServiceResponse<T>is used (default) - If configured → your envelope type becomes the base of generated wrappers
This removes the most common adoption blocker:
"Do we need to change our response model to use this?"
Answer: No.
The platform does not generate your envelope.
It reuses it as a contract dependency — both on the server and client side.
Two usage paths are supported:
1. Springdoc-based (automatic)
- Server starter detects your envelope
- OpenAPI is enriched with required semantics automatically
- No manual schema work is required
2. Spec-first / manual OpenAPI
- Teams can define wrapper schemas directly in OpenAPI
- The wrapper expresses the relationship between the envelope and the payload
- Minimal semantics are added to indicate that the schema represents a generic wrapper
- Client generation reconstructs the correct generic structure from this definition
Example (simplified):
ApiResponseLicenseAccessResponse:
type: object
properties:
data:
$ref: "#/components/schemas/LicenseAccessResponse"
x-api-wrapper: true
x-api-wrapper-datatype: LicenseAccessResponseOptional: internal envelope-related models can be marked to avoid regeneration:
ApiError:
type: object
properties:
errorCode:
type: string
message:
type: string
x-ignore-model: trueSpringdoc is the easiest path — not the only one.
In both approaches, the outcome is the same:
- the envelope remains your contract
- OpenAPI acts as a projection
- generated clients preserve the original type structure
Reuse your own domain models instead of generating them:
<additionalProperties>
<additionalProperty>
openapi-generics.response-contract.CustomerDto=io.example.contract.CustomerDto
</additionalProperty>
</additionalProperties>Result:
- no duplicated DTOs
- full control over model ownership
Switch generation modes through the client build configuration:
<openapi.generics.skip>true</openapi.generics.skip>| Mode | Behavior |
|---|---|
false (default) |
Contract-aware generation |
true |
Standard OpenAPI generation |
Client generation is a controlled execution system:
- upstream templates are patched safely
- contract semantics are injected
- upstream drift fails the build early
Full pipelines are included:
- Spring Boot 3
- Spring Boot 4
- producer → client → consumer
Browse:
You do NOT copy code from this repo.
You only add two building blocks.
<dependency>
<groupId>io.github.blueprint-platform</groupId>
<artifactId>openapi-generics-server-starter</artifactId>
<version>1.0.0</version>
</dependency><parent>
<groupId>io.github.blueprint-platform</groupId>
<artifactId>openapi-generics-java-codegen-parent</artifactId>
<version>1.0.0</version>
</parent>Optional:
<openapi.generics.skip>true</openapi.generics.skip>- Run a sample producer (Spring Boot 3 or 4):
cd samples/spring-boot-3/customer-service
mvn clean package
java -jar target/customer-service-*.jarVerify:
- Swagger UI: http://localhost:8084/customer-service/swagger-ui/index.html
- OpenAPI: http://localhost:8084/customer-service/v3/api-docs.yaml
- Generate a client from the same pipeline:
cd samples/spring-boot-3/customer-service-client
mvn clean install- Inspect generated output:
public class ServiceResponsePageCustomerDto
extends ServiceResponse<Page<CustomerDto>> {}- No duplicated envelope
- Generics preserved
- Contract reused end-to-end
Note: Equivalent pipelines exist under
samples/spring-boot-4/...for Spring Boot 4.
OpenAPI Generics is currently verified with:
- Java: 17+
- Spring Boot: 3.4.x, 3.5.x, 4.x
- springdoc-openapi: 2.8.x (Spring Boot 3.x), 3.x (Spring Boot 4.x)
- OpenAPI Generator: 7.x
- Server scope: Spring WebMvc (
springdoc-openapi-starter-webmvc-ui)
See the full compatibility matrix and support policy: Compatibility & Support Policy
Java Contract (SSOT)
↓
OpenAPI (projection)
↓
Generator (enforcement)
↓
Client (contract-aligned)
OpenAPI is a projection — not the source of truth.
The response envelope is a shared contract, not a generated model.
YourEnvelope<T>
ServiceResponse<T>is the default contract provided by the platform — not a restriction.
The system is designed around a simple principle:
Define your contract once. Preserve it end-to-end.
-
The response envelope is not regenerated per endpoint
-
The same contract is reused across:
- server responses
- OpenAPI projection
- generated clients
-
Client models extend the contract, instead of redefining it
Result:
- no envelope duplication
- no drift between server and client
- a stable, predictable type system
ServiceResponse<T>
ServiceResponse<Page<T>>
YourEnvelope<T>
These shapes are explicitly supported and enforced to guarantee:
- deterministic OpenAPI generation
- predictable client reconstruction
- zero ambiguity in generic resolution
Note: BYOE supports envelopes with a single direct generic payload
(e.g.YourEnvelope<T>).
Nested payloads likeYourEnvelope<Page<T>>are out of scope
and fail fast at application startup.
While ServiceResponse<T> is the canonical default, the platform does not require you to use it.
With BYOE (Bring Your Own Envelope):
YourEnvelope<T>
can be used instead, without changing the overall model.
The behavior remains the same:
- OpenAPI is still a projection
- Generics are still preserved
- Clients still reconstruct the contract shape
For internal architecture and design decisions, see Architecture.
class ServiceResponsePageCustomerDto {
PageCustomerDto data;
Meta meta;
}public class ServiceResponsePageCustomerDto
extends ServiceResponse<Page<CustomerDto>> {}- ✔ Contract identity is preserved
- ✔ Contract ownership is preserved (including the response envelope)
- ✔ Generics are preserved (within supported scope)
- ✔ Client generation is deterministic
- ✔ No contract duplication (external models are reused, not regenerated)
- ✔ Upstream drift is detected early
- openapi-generics-contract
- openapi-generics-server-starter
- openapi-generics-java-codegen
- openapi-generics-java-codegen-parent
- openapi-generics-platform-bom
-
Adoption Guide (GitHub Pages)
Spring Boot OpenAPI Generics — Adoption Guide -
Medium Article
We Made OpenAPI Generator Think in Generics -
RFC 9457
Problem Details for HTTP APIs
Contributions are welcome — especially:
- architectural discussions
- real-world usage feedback
- edge cases and integration insights
If you're evaluating or using the project, your perspective is valuable.
- Open an issue: https://github.com/blueprint-platform/openapi-generics/issues
- Start a discussion: https://github.com/blueprint-platform/openapi-generics/discussions
MIT — see LICENSE


