Skip to content

blueprint-platform/openapi-generics

Repository files navigation

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 typed generic contract from a Spring Boot controller:

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

This is not just a JSON shape.

It is a contract boundary.

ServiceResponse<T> defines the response language.
Page<T> defines the pagination contract.
CustomerDto defines the payload.

A standard OpenAPI client generation flow often turns that boundary into a generated model:

// ❌ Generated by default
class ServiceResponsePageCustomerDto {
  PageCustomerDto data;
  Meta meta;
}

That class may look equivalent, but it is not the same contract anymore.

The envelope has been copied into generated code. The client now owns a generated reinterpretation of a contract that already existed.

With openapi-generics, the generated wrapper keeps the original contract identity:

// ✅ Generated with openapi-generics
public class ServiceResponsePageCustomerDto
    extends ServiceResponse<Page<CustomerDto>> {}

The generated class does not redefine the envelope. It only binds concrete generic parameters to the shared contract type.

Before
default OpenAPI Generator
After
with openapi-generics

This is the core architectural difference:

Default generation:
OpenAPI schema becomes the source for a new envelope model.
OpenAPI Generics:
The Java contract remains the source of truth.
OpenAPI carries enough metadata to reconstruct it.

For one endpoint, the difference is easy to ignore.

Across many generated clients, it becomes expensive.

A BFF, aggregator, or downstream service may consume dozens of APIs. If every generated client brings its own envelope and container copies, each service boundary adds more duplicated models, more mapping code, and more places for contract drift.

OpenAPI Generics takes the opposite position:

The response envelope is not generated output.
It is shared contract authority.

Payload models may be generated.
Endpoint clients may be generated.
Transport code may be generated.

But the envelope should remain owned by the contract.

The result is a generated client that behaves as a thin transport adapter instead of an alternative contract definition.

One envelope. One contract identity. Generics preserved from producer to consumer.

Define your contract once in Java, project it through OpenAPI, and reconstruct it deterministically in the generated client.


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

What's New in 1.2

Version 1.2 builds on the container-aware infrastructure introduced in 1.1 by making the container model extensible and improving generated artifact quality.

Applications can now register their own generic container contracts while continuing to use the same contract-first projection and deterministic client reconstruction pipeline.

Built-in response shapes continue to work unchanged:

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

In addition, applications can now register their own generic container contracts:

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

  containers:
    - type: io.example.contract.Paging
      item-property: content

    - type: io.example.contract.Window
      item-property: items

Configured containers participate in the same projection, metadata generation, and reconstruction pipeline as the built-in container types.

Additional improvements in 1.2 include:

  • application-defined generic container support
  • preservation of Java container identity through x-data-container-type
  • deterministic generated-source hygiene for Java clients
  • cleaner generated Java imports and formatting
  • expanded regression and metadata validation
  • full backward compatibility with existing 1.1 contracts

No migration is required for existing users.


Key Features

Feature What it does Default
BYOE — Bring Your Own Envelope Reuse your existing response envelope (for example ApiResponse<T>) instead of ServiceResponse<T>. No migration required. ServiceResponse<T>
BYOC — Bring Your Own Contract Reuse your existing domain DTOs instead of generating duplicate models. Generate from spec
Application-defined containers Register your own generic container contracts (for example Paging<T> or Window<T>) and have them participate in the same projection, metadata, and reconstruction pipeline as built-in containers. Built-in containers only
Container-aware reconstruction Deterministically reconstruct built-in and configured generic container types from OpenAPI metadata instead of using 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 Apply deterministic template patching, generated-source hygiene, and build-time validation to produce stable, reproducible Java clients. Enabled
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

  # Optional: register application-defined generic containers
  containers:
    - type: io.example.contract.Paging
      item-property: content

    - type: io.example.contract.Window
      item-property: items

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.
  • Application-defined containers are optional and participate in the same projection and reconstruction model as built-in containers when configured.
  • 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 preserves Java generic contract semantics across the OpenAPI lifecycle.

It keeps generic response envelopes, container payloads, and shared DTO contracts aligned from Spring Boot producers to generated Java clients.

The project is built on one principle:

The Java contract is the source of truth.
OpenAPI is a projection of that contract.
Client generation deterministically reconstructs the original contract.

Java Contract (SSOT)
        ↓
OpenAPI Projection
        ↓
Deterministic Client Reconstruction
        ↓
Contract-Aligned Client

In practice this means:

  • the response envelope remains a shared contract, not a generated artifact
  • generated wrapper classes extend existing contracts instead of redefining them
  • OpenAPI carries contract metadata, not contract ownership
  • container semantics are preserved through projection metadata
  • clients and servers remain aligned as contracts evolve

Projection paths

Wrapper semantics can be published in two ways.

Springdoc-based (automatic)

The server starter discovers generic response contracts, projects wrapper schemas, enriches them with contract metadata, and marks infrastructure models so generated clients reconstruct the original Java contract instead of regenerating it.

Spec-first (manual)

Teams that own their OpenAPI documents can publish the same contract semantics directly through the OpenAPI Generics vendor extensions:

  • x-api-wrapper
  • x-api-wrapper-datatype
  • x-data-container
  • x-data-container-type
  • x-data-item
  • x-ignore-model

Together, these extensions describe wrapper semantics, payload type, container identity, item type, and generation behavior, allowing generated clients to reconstruct the original Java contract deterministically without requiring Spring-based projection.

Architecture

OpenAPI Generics contract-first architecture flow

The architecture consists of two complementary phases:

  • Projection — derives deterministic OpenAPI metadata from Java contracts.
  • Reconstruction — restores contract-aligned Java client types during generation using the projected metadata.

Both phases share the same contract authority while keeping generated code isolated from application code.

For internal architecture and design decisions, see the architecture documentation.

Guarantees

  • ✔ Contract identity is preserved across server, OpenAPI, and generated client
  • ✔ Contract ownership remains with your application, not generated code
  • ✔ Built-in and application-defined generic containers share the same projection and reconstruction pipeline
  • ✔ Container identity is preserved through deterministic projection metadata
  • ✔ Generated clients reconstruct contract semantics instead of redefining them
  • ✔ External models can be reused without duplication (BYOC)
  • ✔ Generated source hygiene produces cleaner, deterministic Java artifacts
  • ✔ Upstream OpenAPI Generator changes are detected during the build rather than 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:

  • Generics-aware Java client generation through an OpenAPI Generator specialization
  • Contract metadata via vendor extensions such as x-api-wrapper, x-data-container, x-data-container-type, and x-data-item
  • Server-side OpenAPI enrichment through Springdoc integration
  • Container-aware reconstruction for both built-in and application-defined generic container contracts
  • Deterministic generated-source hygiene for cleaner Java client output

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

Module Responsibility
openapi-generics-contract Shared response contracts and platform-owned generic types.
openapi-generics-server-starter Spring Boot integration that projects generic contract metadata into OpenAPI.
openapi-generics-java-codegen OpenAPI Generator specialization that reconstructs contract-aligned Java clients.
openapi-generics-java-codegen-parent Maven parent for client generation, template patching, and generated-source hygiene.
openapi-generics-platform-bom Dependency alignment for OpenAPI Generics modules.

References

Project Documentation

  • Rationale: Why OpenAPI Generics Exists
    Explains the architectural motivation, design decisions, engineering trade-offs, and ecosystem constraints behind the project.

  • Documentation Site
    Official GitHub Pages documentation for adoption guides, architecture, compatibility, and project usage.

Articles

Standards & Specifications


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