Skip to content

blueprint-platform/openapi-generics

OpenAPI Generics for Spring Boot — Keep Your API Contract Intact End-to-End

Build CodeQL codecov

Release Maven Central

Java Spring Boot OpenAPI Generator

License: MIT

Generics-Aware OpenAPI Contract Lifecycle

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.


Table of Contents


The problem in 30 seconds

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.


Get started

1. Try it in 2 minutes

Runnable end-to-end sample stacks are available under samples, including Spring Boot 3, Spring Boot 4, and dedicated type-coverage samples covering supported payload types, wrapper shapes, and generic response patterns.

samples
├── domain-contracts
├── spring-boot-3
│   ├── customer-service
│   ├── customer-service-client
│   └── customer-service-consumer
├── spring-boot-4
│   ├── customer-service
│   ├── customer-service-client
│   └── customer-service-consumer
└── type-coverage
    ├── service-response
    └── byoe-response

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-*.jar

Verify it's running:

Generate the client from the same pipeline:

cd samples/spring-boot-3/customer-service-client
mvn clean install

Inspect the generated wrapper:

public class ServiceResponsePageCustomerDto
    extends ServiceResponse<Page<CustomerDto>> {}

No duplicated envelope. Generics preserved. Contract reused end-to-end.


2. Use it in your project

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.1.0</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.1.0</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.


Real-World Example

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


What's New in 1.1

Version 1.1 expands the range of supported generic response contracts.

In addition to single-payload responses, OpenAPI Generics now supports collection and paging containers while preserving the same contract-first generation model.

Supported response shapes now include:

ServiceResponse<T>

ServiceResponse<List<T>>

ServiceResponse<Set<T>>

ServiceResponse<Page<T>>

The same support is available when using your own shared response envelope:

ApiResponse<T>

ApiResponse<List<T>>

ApiResponse<Set<T>>

ApiResponse<Page<T>>

Page<T> refers to the paging contract provided by:

io.github.blueprintplatform.openapi.generics.contract.paging.Page<T>

This means generated clients can now reconstruct a broader range of real-world API contracts while continuing to reuse existing envelopes and shared domain models.

Additional improvements in 1.1 include:

  • deterministic OpenAPI specification validation
  • OpenAPI snapshot verification in sample projects
  • expanded end-to-end validation coverage
  • stronger regression detection in CI pipelines

All 1.0.x contracts remain fully supported.

No migration is required for existing users.


Key Features

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
Container-aware reconstruction Reconstruct supported container types through a shared container metadata model instead of container-specific generation logic. Enabled
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. 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, client, and consumer pipelines for Spring Boot 3, Spring Boot 4, ServiceResponse, and BYOE scenarios. See samples

BYOE — Bring Your Own Envelope

Already have an ApiResponse<T> (or another response envelope) shared across your services?

Use it as the contract source of truth without migrating to a platform-specific wrapper.

On the server/producer side:

openapi-generics:
  envelope:
    type: io.example.contract.ApiResponse

On the client/codegen side:

<additionalProperties>
  <additionalProperty>
    openapi-generics.envelope=io.example.contract.ApiResponse
  </additionalProperty>
</additionalProperties>

Key characteristics:

  • Your envelope remains the contract owner.
  • Generated wrappers extend your envelope instead of redefining it.
  • The envelope type must be available on the client classpath.
  • Springdoc-based projection is automatic.
  • Spec-first pipelines can publish equivalent semantics through OpenAPI vendor extensions.

BYOC — Bring Your Own Contract

Reuse DTOs you already own instead of generating duplicate models.

Map OpenAPI model names to existing Java types:

<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 mapping follows:

openapi-generics.response-contract.<OpenAPI model name>=<fully-qualified Java type>

The generated client imports and reuses those contract types directly instead of producing duplicate DTO definitions.


Fallback to Standard Generation

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:

<generatorName>java</generatorName>
openapi.generics.skip Behavior
false (default) Apply generics-aware template patching
true Skip generics-aware template patching

Use this mode for output comparison, troubleshooting, or temporary opt-out scenarios.


How it works

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

Projection paths

Wrapper semantics can be published in two ways:

  1. 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.
  2. 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.

Architecture

OpenAPI Generics contract-first architecture flow

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

Guarantees

  • ✔ Contract identity is preserved across server, spec, and client
  • ✔ Contract ownership stays with you (envelope and DTOs are reusable, not duplicated)
  • ✔ Supported generic structures are preserved across projection and reconstruction
  • ✔ 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

Compatibility

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


Relationship to OpenAPI Generator

OpenAPI Generics is not a fork of OpenAPI Generator.

It builds on top of the upstream project and adds a Java/Spring Boot specialization layer focused on preserving contract-owned generic structures across the OpenAPI lifecycle.

The generated OpenAPI document remains valid OpenAPI and can be consumed by standard OpenAPI tooling without modification.

What OpenAPI Generics adds:

  • Generic-aware client generation through a custom JavaClientCodegen
  • Contract metadata via vendor extensions such as x-api-wrapper, x-data-container, and x-data-item
  • Server-side OpenAPI enrichment through Springdoc integration
  • Container-aware reconstruction for supported container types (Page<T>, List<T>, Set<T>)

The project keeps OpenAPI Generator as the source of template structure and applies a minimal generics-aware extension layer rather than maintaining a forked template set.

In short:

Java Contract
      ↓
OpenAPI Projection
      ↓
Deterministic Client Reconstruction

OpenAPI Generics preserves contract semantics while remaining fully compatible with the OpenAPI ecosystem.

Generator Version Ownership

OpenAPI Generics does not lock consumers to a specific OpenAPI Generator version.

The provided parent configuration includes a tested default, but consumers may override the openapi-generator.version property within the supported 7.x range.

OpenAPI Generics owns contract semantics. Consumers own the OpenAPI Generator version they choose to run.


Modules


References


Contributing

OpenAPI Generics is still evolving, and real-world feedback is the most valuable input for future improvements.

Whether you're evaluating the project, running it in a prototype, or using it in production, feedback is welcome.

Questions, bug reports, design discussions, and adoption experiences all help shape the roadmap.

For contributions and feedback:

  • 🐛 Bugs and issues → Issues
  • 💡 Design discussions and feature ideas → Discussions
  • 🔗 Private feedback → LinkedIn

If OpenAPI Generics helped solve a real problem in your environment, hearing about that experience is often just as valuable as a code contribution.


License

MIT — see LICENSE


Barış Saylı GitHub · Medium · LinkedIn