OpenAPI Generics β€” Keep Your API Contract Intact End-to-End

Define your API once in Java.
Preserve it across OpenAPI and generated clients β€” without duplication or drift.


Table of Contents


Why this exists

In most OpenAPI-based workflows:

  • generics are flattened or lost
  • response envelopes are regenerated per endpoint
  • clients gradually drift from server-side contracts

Over time, this creates a gap between what your API defines and what your clients consume.

The result is not an immediate failure β€” but a slow erosion of contract integrity.

This platform removes that entire class of problems.

Your Java contract remains the single source of truth β€” across all layers.


What you actually do

You don’t configure OpenAPI.
You don’t maintain templates.
You don’t fight generator behavior.

You only do two things:

  1. return your contract from controllers
  2. generate clients from OpenAPI

That’s it.

The platform handles projection, generation, and contract alignment automatically.


⚑ Quick Start

1. Server (producer)

Add the dependency:

<dependency>
  <groupId>io.github.blueprint-platform</groupId>
  <artifactId>openapi-generics-server-starter</artifactId>
  <version>0.8.1</version>
</dependency>

Return your contract:

ServiceResponse<CustomerDto>

2. Client (consumer)

Inherit the parent:

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

Generate the client:

mvn clean install

Result

ServiceResponse<CustomerDto>

The exact same contract type flows from server to client.

  • no duplicated models
  • generics preserved end-to-end
  • contract types reused (not regenerated)

Proof β€” Generated Client (Before vs After)

Before (default OpenAPI behavior)

  • duplicated envelope per endpoint
  • generics flattened or lost
  • unstable and verbose model graph

After (contract-aligned generation)

public class ServiceResponsePageCustomerDto
    extends ServiceResponse<Page<CustomerDto>> {}
  • no envelope duplication
  • generics preserved end-to-end
  • contract types reused directly

What changed

Instead of generating new models from OpenAPI:

  • the contract is preserved
  • wrappers are generated as thin type bindings
  • the client reuses existing domain semantics

Result:

Java Contract (SSOT)
        ↓
OpenAPI (projection, not authority)
        ↓
Generator (deterministic reconstruction)
        ↓
Client (canonical contract types)

No reinterpretation. No duplication. No drift.


What is actually generated

The client does not recreate your models.

Instead, it generates thin wrapper classes that bind OpenAPI responses back to your canonical contract.

Example:

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

Key properties:

  • no envelope duplication
  • no structural redefinition
  • no generic type loss

These classes exist only to bridge OpenAPI β†’ Java type system.


How you actually use it

You never interact with generated wrappers directly.

Instead, you define an adapter layer:

public interface CustomerClientAdapter {

  ServiceResponse<CustomerDto> createCustomer(CustomerCreateRequest request);

  ServiceResponse<CustomerDto> getCustomer(Integer customerId);

  ServiceResponse<Page<CustomerDto>> getCustomers();

}

Implementation delegates to generated API:

@Service
public class CustomerClientAdapterImpl implements CustomerClientAdapter {

  private final CustomerControllerApi api;

  public CustomerClientAdapterImpl(CustomerControllerApi api) {
    this.api = api;
  }

  @Override
  public ServiceResponse<CustomerDto> getCustomer(Integer customerId) {
    return api.getCustomer(customerId);
  }

  @Override
  public ServiceResponse<Page<CustomerDto>> getCustomers() {
    return api.getCustomers(null, null, 0, 5, "customerId", "ASC");
  }
}

What this gives you

At usage level, your application only sees:

ServiceResponse<CustomerDto>
ServiceResponse<Page<CustomerDto>>

Not:

  • generated wrapper classes
  • duplicated DTO hierarchies
  • OpenAPI-specific models

No translation layer. No reinterpretation. No drift.


Why this matters

Traditional OpenAPI generation produces:

  • duplicated response envelopes
  • flattened generics
  • unstable model graphs

This approach guarantees:

  • a single contract shared across all layers
  • stable and predictable client generation
  • zero drift between server and client semantics

Mental model

Think of generated classes as:

thin type adapters β€” not models

They exist because OpenAPI cannot express Java generics β€” not because your model requires them.

They simply reconnect OpenAPI output back to your existing contract, without redefining it.

Your system always operates on:

ServiceResponse<T>

Everything else is just infrastructure.


Next steps



Final note

If the contract stays consistent, everything stays consistent.

This system works by keeping that boundary intact.


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