OpenAPI Generics for Spring Boot

Keep your API contract intact end-to-end — Java types, OpenAPI document, generated clients.

OpenAPI Generics is a deterministic, generics-aware API platform for Spring Boot. It treats your Java code as the source of truth for the contract, and OpenAPI as a faithful projection of it. The result: generic response shapes survive intact across the server, the spec, and every generated client — without duplicated envelope models, lost type parameters, or drift between layers.

Get started · View on GitHub


The problem in 30 seconds

You write a clean controller:

ResponseEntity<ServiceResponse<Page<CustomerDto>>> getCustomers(...)

Springdoc projects it into OpenAPI. OpenAPI Generator produces a Java client. And then this happens:

// Generated client — generics gone, envelope duplicated per endpoint
public class ServiceResponsePageCustomerDto {
  private List<CustomerDto> data;
  private Meta meta;
  // getters, setters, @JsonProperty, full envelope re-implementation...
}

public class ServiceResponsePageOrderDto {
  private List<OrderDto> data;        // same shape
  private Meta meta;                  // same Meta
  // ...regenerated again, per endpoint
}

Every endpoint gets its own copy of the envelope. The <T> is gone. Your client team writes the same wrapper logic on every project. The contract you thought was unified is now scattered across dozens of generated classes.

This is what OpenAPI Generics fixes.


Proof — generated client, before vs after

The two screenshots below are from a real generated client module — same OpenAPI document, same generator version, only the contract-aware pipeline is toggled.

Before — default OpenAPI Generator

Generated client without openapi-generics — duplicated envelope models, flattened generics

  • envelope materialized as a full class per endpoint
  • <T> flattened — getData() returns a fused type that needs casting
  • model graph grows linearly with the number of endpoints

After — with openapi-generics

Generated client with openapi-generics — thin wrappers extending the contract envelope

public class ServiceResponsePageCustomerDto
    extends ServiceResponse<Page<CustomerDto>> {}
  • one shared envelope, imported from the contract module
  • generics preserved end-to-end — ServiceResponse<Page<CustomerDto>> survives intact
  • externally owned DTOs reused directly via BYOC — no duplication

What you get instead

The same controller, with openapi-generics:

// Generated client — generics preserved, envelope shared
public class ServiceResponsePageCustomerDto
        extends ServiceResponse<Page<CustomerDto>> {}

public class ServiceResponsePageOrderDto
        extends ServiceResponse<Page<OrderDto>> {}

One envelope, one Meta, one Page. Wrappers are thin type bindings. Your DTOs come from your existing classpath, not from regeneration. The contract on the server, in the OpenAPI document, and in every generated wrapper is the same shape.


Key features

Feature What it does
Contract-first projection Your Java types are the source of truth; OpenAPI is a faithful projection, not a separate contract you maintain by hand.
Generics preserved ServiceResponse<Page<CustomerDto>> survives intact through the OpenAPI document and into every generated client.
BYOE — Bring Your Own Envelope Use ServiceResponse<T> out of the box, or plug in your own envelope (ApiResponse<T>, Result<T>, …). Validated at startup, no silent degradation.
BYOC — Bring Your Own Contract Reuse externally owned DTOs from a shared contract module instead of regenerating them client-side. Zero duplication.
Deterministic & fail-fast Same input + same configuration → byte-identical output across builds. Misconfiguration fails at boot or build time, never silently.
Fallback to standard generation Disable the generics-aware pipeline with openapi.generics.skip, or switch to generatorName=java for full stock OpenAPI Generator behavior.
Zero-drift template patching The codegen pipeline patches the upstream model.mustache at build time — no frozen template snapshots, no silent drift across generator versions.

How it works

Two halves of a single contract:

        ┌──────────────────────────────────┐
        │     Java contract (source        │
        │     of truth — your code)        │
        └────────────┬─────────────────────┘
                     │
            projects (server)
                     ▼
        ┌──────────────────────────────────┐
        │   OpenAPI document with vendor   │
        │   extensions:                    │
        │     x-api-wrapper                │
        │     x-data-container             │
        │     x-data-item                  │
        │     x-ignore-model               │
        └────────────┬─────────────────────┘
                     │
            reconstructs (client)
                     ▼
        ┌──────────────────────────────────┐
        │   Generated Java client —        │
        │   thin wrappers extending the    │
        │   contract, generics preserved   │
        └──────────────────────────────────┘

On the server, a Springdoc customizer inspects controller return types, identifies envelope shapes, and stamps the OpenAPI document with vendor extensions that describe the original generic structure. Envelope and infrastructure schemas (ServiceResponse, Page, Meta) are marked x-ignore-model so the client knows not to regenerate them.

On the client, a Maven parent POM orchestrates a five-stage build pipeline (extract → patch → overlay → generate → register sources). The java-generics-contract generator reads the vendor extensions and reconstructs the original generic shape — emitting wrappers as thin extends bindings rather than duplicated full classes.

For the full pipeline mechanics, see Architecture.


Get started

Producer service (server-side)

Add the starter to your Spring Boot service:

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

Write your controllers normally — ResponseEntity<ServiceResponse<CustomerDto>>, ResponseEntity<ServiceResponse<Page<CustomerDto>>>, async variants — and the projection runs automatically. No annotations, no customizer registration, no OpenAPI hand-editing.

Server-Side Adoption Guide

Client module (client-side)

Inherit the codegen parent in your client module:

<parent>
  <groupId>io.github.blueprint-platform</groupId>
  <artifactId>openapi-generics-java-codegen-parent</artifactId>
  <version>1.0.2</version>
</parent>

Configure the OpenAPI Generator plugin with <generatorName>java-generics-contract</generatorName>, point inputSpec at your producer’s OpenAPI document, and run mvn clean install. Generated wrappers extend your contract directly.

Client-Side Adoption Guide

Working samples

The repository ships runnable end-to-end stacks for both Spring Boot 3 and Spring Boot 4:

samples/spring-boot-3/    ← producer + client + consumer (BYOC enabled)
samples/spring-boot-4/    ← producer + client + consumer (zero-config default flow)

Each stack runs with docker compose up --build -d from its directory. The two stacks intentionally differ — they show that BYOE and BYOC are optional alignment inputs, not requirements. See samples/README.md for the full run-and-verify flow.


Compatibility

  • Java 17+ (samples use Java 21)
  • Spring Boot 3.4.x, 3.5.x, 4.x — WebMvc only
  • springdoc-openapi WebMvc starter — 2.8.x for Boot 3, 3.x for Boot 4
  • OpenAPI Generator 7.x — Maven-based client generation

WebFlux, Gradle, and non-Java server frameworks are deliberately out of scope. The platform boundary is documented in detail.

Compatibility & Support Policy


Relationship to OpenAPI Generator

OpenAPI Generics is not a fork of OpenAPI Generator. It uses the upstream openapi-generator-maven-plugin 7.x as its execution engine and patches the upstream model.mustache at build time — surgically, with a single regex insertion that injects the generic-aware branch into the foundational schema loop.

If the upstream template structure ever changes in a way that the patch can’t apply, the build fails immediately with a clear error. There is no frozen template snapshot, no parallel generator implementation, no maintenance burden compounding with each upstream release.

What stays upstream:

  • the generator itself
  • the HTTP client libraries (restclient, webclient, resttemplate, …)
  • API operation generation
  • Mustache template foundations

What this platform adds:

  • vendor extension protocol (x-api-wrapper, x-data-container, x-data-item, x-ignore-model)
  • GenericAwareJavaCodegen — a thin extension of JavaClientCodegen
  • contract-aware wrapper templates (overlaid on patched upstream)
  • BYOE / BYOC resolution at generation time
  • the build pipeline that orchestrates extract → patch → overlay → generate → register

Documentation


Community

  • 💬 GitHub Discussions — design questions, edge cases, OAS 3.1 compliance
  • 🐛 GitHub Issues — bug reports, feature requests
  • ⭐ Star the repo if openapi-generics solves a problem you’ve had

The project is open source under MIT license. Contributions, feedback, and adoption stories are all welcome.


This site uses Just the Docs, a documentation theme for Jekyll.