Prevent OpenAPI Generator from redefining your Java contract.
A contract-preserving OpenAPI Generator specialization for Java/Spring that keeps shared envelopes and DTOs reusable across service boundaries — no model explosion, no manual templates, no fork.
- The problem in 30 seconds
- Get started
- Real-World Example
- Key features in 1.0.x (GA)
- How it works
- Compatibility
- Relationship to OpenAPI Generator
- Modules
- References
- Contributing
- License
You return a generic envelope from a Spring Boot controller:
ResponseEntity<ServiceResponse<Page<CustomerDto>>> getCustomers() { ... }OpenAPI Generator gives your clients this:
// ❌ Generated by default — one of these per endpoint
class ServiceResponsePageCustomerDto {
PageCustomerDto data;
Meta meta;
}With openapi-generics, the same client looks like this:
// ✅ Generated with openapi-generics
public class ServiceResponsePageCustomerDto
extends ServiceResponse<Page<CustomerDto>> {}The difference looks small.
Architecturally, it is not.
| Before default OpenAPI Generator |
After with openapi-generics |
![]() |
![]() |
The generated class on the left owns the envelope structure.
The generated class on the right only binds generic parameters to an existing contract.
That distinction changes how contracts move across service boundaries.
Producer Service
↓
Java Contract
↓
OpenAPI Projection
↓
Generated Client
↓
Consumer Service
↓
Consumer API
Instead of generating a new envelope hierarchy at every service boundary, the generated client reconstructs the original contract shape so downstream services can continue using the same envelope and payload types.
This becomes increasingly important in microservice architectures.
A BFF, aggregator, or downstream service may consume dozens of generated clients. Without contract reuse, every hop introduces duplicated envelopes, duplicated DTOs, and additional mapping layers whose only purpose is translating between structurally equivalent models.
OpenAPI Generics takes a different approach.
Contract-owned types remain contract-owned.
The envelope is reused rather than regenerated. External DTOs can be reused through BYOC. Generated clients become thin transport adapters instead of alternative contract definitions.
The result is that contract identity survives service boundaries without being continuously redefined.
One contract-owned envelope. Generics preserved. Contract ownership remains intact from producer to consumer without recreating the model at every hop.
Define your contract once in Java, project it through OpenAPI, and reconstruct it deterministically across every generated client and downstream service.
Runnable end-to-end sample stacks are available under samples.
Each stack contains:
- a producer service,
- a generated client,
- and a downstream consumer.
See samples/README.md for the full topology, Docker-based setup, and stack overview.
Run a sample producer (Spring Boot 3; equivalent pipeline under samples/spring-boot-4/):
cd samples/spring-boot-3/customer-service
mvn clean package
java -jar target/customer-service-*.jarVerify it's running:
- Swagger UI — http://localhost:8084/customer-service/swagger-ui/index.html
- OpenAPI — http://localhost:8084/customer-service/v3/api-docs.yaml
Generate the client from the same pipeline:
cd samples/spring-boot-3/customer-service-client
mvn clean installInspect the generated wrapper:
public class ServiceResponsePageCustomerDto
extends ServiceResponse<Page<CustomerDto>> {}No duplicated envelope. Generics preserved. Contract reused end-to-end.
You don't copy code from this repo — you add two building blocks.
Server (producer):
<dependency>
<groupId>io.github.blueprint-platform</groupId>
<artifactId>openapi-generics-server-starter</artifactId>
<version>1.0.2</version>
</dependency>Important
openapi-generics-server-starter does not intercept application requests or change endpoint runtime behavior.
It is invoked only when Springdoc generates the OpenAPI document, for example when /v3/api-docs or /v3/api-docs.yaml is requested, or when the document is generated in CI.
If the OpenAPI document is never generated, this component does nothing.
Client (consumer):
<parent>
<groupId>io.github.blueprint-platform</groupId>
<artifactId>openapi-generics-java-codegen-parent</artifactId>
<version>1.0.2</version>
</parent>The client generation flow uses the java-generics-contract generator
instead of the standard java generator to preserve generic wrapper semantics.
That's it. Run your service, generate the OpenAPI document, generate the client, and get contract-aligned wrappers.
For BYOE, BYOC, and fallback-to-standard-generation options, see the Key features section below.
See the Licensing Project for a complete end-to-end BYOE example
using a shared ApiResponse<T> contract.
The project demonstrates:
- Spring Boot server integration with
openapi-generics-server-starter - Contract-first OpenAPI projection
- Generated Java client using
openapi-generics-java-codegen-parent - Shared
ApiResponse<T>reuse across service, client, SDK, and CLI - Docker-based end-to-end verification
🔗 https://github.com/bsayli/licensing
| Feature | What it does | Default |
|---|---|---|
| BYOE — Bring Your Own Envelope | Use your existing response envelope (for example ApiResponse<T>) instead of ServiceResponse<T>. No migration required. |
ServiceResponse<T> |
| BYOC — Bring Your Own Contract | Reuse your own domain DTOs instead of generating duplicate models. | Generate from spec |
| Fallback to standard generation | Disable the generics-aware template patching with a single Maven property. To fully revert to stock OpenAPI Generator behavior, switch the client module to generatorName=java. Useful for comparison, debugging, or temporary opt-out. |
Generics-aware generation enabled |
| Deterministic generation | Upstream OpenAPI Generator templates are extracted on every build, patched with a single generics-aware branch, and the build fails fast if the upstream template structure changes. | — |
| End-to-end samples | Complete producer and client pipelines for Spring Boot 3 and Spring Boot 4. | See samples |
Already have an ApiResponse<T> (or any other envelope) across your services?
Use it as the shared contract on both sides — no rewrites.
On the server/producer side, configure the envelope so the starter can project it into the OpenAPI document:
openapi-generics:
envelope:
type: io.example.contract.ApiResponseOn the client/codegen side, configure the same envelope so generated wrappers extend your contract type:
<additionalProperties>
<additionalProperty>
openapi-generics.envelope=io.example.contract.ApiResponse
</additionalProperty>
</additionalProperties>- If unset →
ServiceResponse<T>is used as the default envelope. - If set → your envelope becomes the base of every generated wrapper.
- The envelope type must be available on the client module classpath, usually via a shared contract dependency.
- Custom envelope types are used as generated wrapper base classes, so they should be subclass-friendly: provide a public no-argument constructor and public accessors for the envelope properties.
- With Springdoc, the server starter projects wrapper semantics automatically.
Spec-first pipelines can declare the same semantics via
x-api-wrapperextensions in OpenAPI directly.
Scope: BYOE supports envelopes with a single direct generic payload (
YourEnvelope<T>). Nested forms likeYourEnvelope<Page<T>>are out of scope and fail fast at startup — see Restricted generic depth for the design rationale.
Stop regenerating DTOs you already own. Map each OpenAPI model name to the Java type you want the generated client to reuse:
<additionalProperties>
<additionalProperty>
openapi-generics.response-contract.CustomerDto=io.example.contract.CustomerDto
</additionalProperty>
<additionalProperty>
openapi-generics.response-contract.AddressDto=io.example.contract.AddressDto
</additionalProperty>
<additionalProperty>
openapi-generics.response-contract.OrderDto=io.example.contract.OrderDto
</additionalProperty>
</additionalProperties>Each property maps:
openapi-generics.response-contract.<OpenAPI model name> = <fully-qualified Java type>
The generated client imports those existing contract types directly instead of producing near-duplicate DTO models.
BYOC applies to the payload types used inside generated wrappers, including
nested generic structures such as ServiceResponse<Page<CustomerDto>>.
Disable the generics-aware template patching with a single Maven property:
<openapi.generics.skip>true</openapi.generics.skip>This skips the template extraction, patching, and overlay steps provided by
openapi-generics-java-codegen-parent.
To fully revert to stock OpenAPI Generator behavior, also change the client module configuration to:
<generatorName>java</generatorName>openapi.generics.skip |
Behavior |
|---|---|
false (default) |
Apply generics-aware template patching |
true |
Skip generics-aware template patching |
Use this mode to compare generated output, troubleshoot generation issues, or temporarily opt out of the generics-aware customization for a specific client module.
OpenAPI Generics is not primarily a generics solution.
It is a contract preservation system that happens to use Java generics as the mechanism.
The project is built on one principle:
The Java contract is the source of truth. OpenAPI is a projection of that contract. Client generation is a deterministic reconstruction of it.
Java Contract (SSOT)
↓
OpenAPI (projection — not authority)
↓
Generator (deterministic reconstruction)
↓
Client (contract-aligned types)
In practice this means:
- the response envelope is a shared contract, not a generated artifact
- generated client classes extend that contract instead of redefining it
- OpenAPI carries metadata (
x-api-wrapper,x-data-container), not authority - clients and servers stay aligned even as the spec evolves
Wrapper semantics can be published in two ways:
- Springdoc-based (automatic) — the server starter detects your generic envelope, creates wrapper schemas, and marks contract-owned infrastructure models so the client does not regenerate them.
- Spec-first (manual) — teams can define wrapper schemas directly in OpenAPI using the same vendor extensions (
x-api-wrapper,x-data-item,x-ignore-model).
Both approaches produce the same result: the envelope remains your contract, OpenAPI acts as a projection, and generated clients preserve the original generic structure.
The diagram shows two parallel phases — projection (server → spec) and enforcement (spec → client) — both rooted in a single shared authority layer. The adapter boundary keeps generated code isolated from application logic.
For internal architecture and design decisions: architecture
- ✔ Contract identity is preserved across server, spec, and client
- ✔ Contract ownership stays with you (envelope and DTOs are reusable, not duplicated)
- ✔ Generics are preserved within the supported scope
- ✔ Client generation is deterministic — same spec, same output, every build
- ✔ External models are reused, not regenerated
- ✔ Upstream OpenAPI Generator drift is detected at build time, not at runtime
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
This is not a fork of OpenAPI Generator. It uses the upstream tool as a Maven dependency and adds a Java/Spring Boot specialization layer on top.
What stays upstream:
- OpenAPI Generator (used as-is, fresh extraction on every build)
- OpenAPI 3.x spec (only
x-vendor extensions added) - The full upstream template chain
What this project adds:
- A custom generator extending
JavaClientCodegen - A surgical patch to upstream
model.mustachethat injects a single generic-aware branch — the rest is untouched - Vendor extensions (
x-api-wrapper,x-data-container) carrying generic semantics through the spec - Server-side
OpenApiCustomizerfor contract introspection
Why not just drop a custom model.mustache into templateDirectory?
That approach freezes a snapshot of the upstream template and quietly
falls behind as upstream evolves. This project keeps upstream as the
source of structure, injects only the generic-aware branch, and fails
the build fast if upstream changes invalidate the patch.
Cross-language parity is an explicit non-goal. Java generics deserve a generics-aware solution; other languages may benefit from different specializations on top of the same upstream.
OpenAPI Generics does not lock consumers to a specific OpenAPI Generator release.
The openapi-generics-java-codegen-parent provides a tested default through the
openapi-generator.version Maven property. Consumers can override that property
in their own client module:
<properties>
<openapi-generator.version>7.22.0</openapi-generator.version>
</properties>The same property is used by the parent for:
- extracting the upstream
model.mustache - wiring
openapi-generator-maven-plugin - resolving
openapi-generatorandopenapi-generator-coreplugin dependencies
This keeps the client generation execution context version-coherent while still allowing consumers to choose the OpenAPI Generator version within the supported 7.x line.
For example, a client project may inherit
openapi-generics-java-codegen-parent while selecting its own OpenAPI Generator
version through the standard Maven property mechanism.
This is intentional.
OpenAPI Generics specializes generation behavior; it does not take ownership of the OpenAPI Generator lifecycle itself.
OpenAPI Generics owns the contract semantics. Consumers own the generator version they choose to run.
- 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
The project is in early adoption and v1.0.1 just shipped. The most useful thing right now is hearing from people who are actually pulling it into a build.
If you've tried it — even briefly, even just evaluated — there's a pinned discussion with the questions I'd most like answered:
👉 v1.0.1 is out — and I'd like to hear how you're using openapi-generics
It covers BYOE friction, BYOC patterns, error model choice (RFC 9457 vs envelope-based), and Spring Boot 3 vs 4 experience. Two sentences are enough. Anonymous or company-redacted feedback is fine.
For everything else:
- 🐛 Concrete bugs → Issues
- 💡 Design suggestions → Ideas
- 🔗 Private feedback → LinkedIn DM
A 👍 reaction on Discussion #20 is itself a useful signal — it tells me someone real is using this without requiring you to share details publicly.
MIT — see LICENSE


