Client-Side Adoption β Contract-First Client Integration
Generate a Java client that preserves contract semantics exactly as published β no duplication, no reinterpretation, no drift.
This is not a typical OpenAPI client setup.
It defines a controlled build-time system where:
- OpenAPI is treated as input (not authority)
- the contract is preserved (not regenerated)
- and the output is deterministic across environments
This guide defines the correct client-side integration model for the platform.
It focuses on three things only:
- consuming OpenAPI as input
- executing a controlled build pipeline
- using the generated client safely
Everything else is intentionally handled by the platform.
π Table of Contents
- β‘ 60-second quick start
- π― What the client actually does
- π₯ Input: OpenAPI (not your contract)
- π¦ Minimal setup
- π Build pipeline (what really happens)
- π§ Output: what gets generated
- π Usage: how the client enters your system
- π§± Adapter boundary (strongly recommended)
- π Quick verification
- β οΈ Error handling
- π§ Mental model
- π§Ύ Summary
β‘ 60-second quick start
You want:
- a type-safe client
- zero duplicated models
- preserved
ServiceResponse<T>semantics
Do this:
1) Inherit the parent
<parent>
<groupId>io.github.blueprintplatform</groupId>
<artifactId>openapi-generics-java-codegen-parent</artifactId>
</parent>
2) Provide OpenAPI
/v3/api-docs.yaml
3) Build
mvn clean install
Thatβs it.
π― What the client actually does
The client has one responsibility:
Convert an OpenAPI document into a contract-aligned Java client without redefining anything.
It does not:
- design models
- interpret business semantics
- introduce abstractions
It only executes a deterministic transformation:
OpenAPI β deterministic Java client
π₯ Input: OpenAPI (not your contract)
Client generation always starts from an existing OpenAPI document.
curl http://localhost:8084/.../v3/api-docs.yaml -o api.yaml
Critical distinction:
OpenAPI is input metadata, not the contract itself.
Implication:
- structure comes from OpenAPI
- semantics come from shared contract types
π¦ Minimal setup
You need two things only. Responsibilities are strictly separated.
1. Build-time orchestration (MANDATORY)
<parent>
<groupId>io.github.blueprintplatform</groupId>
<artifactId>openapi-generics-java-codegen-parent</artifactId>
<version>0.8.1</version>
</parent>
This is the entry point of the system.
It provides everything required for generation:
- generator binding (
java-generics-contract) - template pipeline (extract β patch β overlay)
- contract-aware import mappings
- deterministic execution model
- generated sources registration
Important:
You do NOT add or configure internal dependencies. The parent already wires the system.
This includes the contract dependency.
If it is needed, it is already managed by the parent/BOM β not by you.
2. OpenAPI Generator plugin (USER INPUT ONLY)
Minimal working configuration:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-client</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/your-api-docs.yaml</inputSpec>
<library>your-library-choice</library>
<apiPackage>your.api.package</apiPackage>
<modelPackage>your.model.package</modelPackage>
<invokerPackage>your.invoker.package</invokerPackage>
<configOptions>
<useSpringBoot3>true</useSpringBoot3>
<serializationLibrary>your-choice</serializationLibrary>
<openApiNullable>false</openApiNullable>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
What you control here:
- input OpenAPI spec
- HTTP client (
library) - package structure
- serialization strategy
What you do NOT control:
- generator internals
- template system
- contract mappings
π Build pipeline (what really happens)
This system is not configuration-driven. It is a controlled execution pipeline.
Full pipeline:
OpenAPI spec (input)
β
Parent POM (orchestration)
β
Template extraction (upstream)
β
Template patch (api_wrapper injection)
β
Template overlay (custom templates)
β
Custom generator (java-generics-contract)
β
Generated sources (contract-aligned)
Important:
Each step is fixed and ordered. No user-defined hooks exist in this pipeline.
What the platform enforces
The build guarantees:
- contract models are NOT generated
- wrapper classes are generated deterministically
- generics are preserved (
ServiceResponse<T>,Page<T>) - OpenAPI is interpreted β not materialized as independent models
Generated sources integration
Generated code is automatically:
- written to
target/generated-sources/openapi - added to the Maven compilation lifecycle
No manual configuration is required.
π§ Configuration boundaries
The system is intentionally split into two control zones.
User-controlled (safe)
<inputSpec>...</inputSpec>
<library>...</library>
<apiPackage>...</apiPackage>
<modelPackage>...</modelPackage>
<invokerPackage>...</invokerPackage>
<openapi-generator.version>...</openapi-generator.version>
These control:
- input specification
- HTTP transport layer
- package structure
- generator version
Platform-controlled (DO NOT override)
The parent already provides:
- generator name (
java-generics-contract) - template directory
- import mappings
- template patching pipeline
- model suppression rules
These ensure:
- contract preservation (
ServiceResponse<T>,Page<T>) - deterministic wrapper generation
- zero duplication of platform models
Critical rule
If you override platform-controlled settings, you leave the contract-safe execution path.
π« What users should NOT do
Do NOT:
- add internal platform dependencies manually
- override templates
- change generator name
- modify import mappings
- inject custom model logic
Reason:
The system is intentionally controlled to guarantee determinism and contract alignment.
π§ Output: what gets generated
From OpenAPI schema:
ServiceResponseCustomerDto
Generated code:
class ServiceResponseCustomerDto extends ServiceResponse<CustomerDto>
Properties:
- thin wrappers only
- no duplicated envelopes
- direct reuse of contract types
Important:
The wrapper exists only to rebind generics β it does not introduce new structure.
Implication:
- no additional fields are created
- no behavior is added
- no contract semantics are modified
The generated layer is purely structural β it restores type information that OpenAPI cannot represent directly.
π Usage: how the client enters your system
Generated sources:
target/generated-sources/openapi
Usage:
ServiceResponse<CustomerDto>
At this point:
No additional mapping layer is required for correctness.
- type system is preserved
- contract is intact
- client is aligned with producer
π§± Adapter boundary (strongly recommended)
Do not expose generated APIs directly.
Define a boundary:
public interface CustomerClient {
ServiceResponse<CustomerDto> getCustomer(Long id);
}
Purpose:
- isolate generation details
- protect domain logic
- enable safe evolution
π Quick verification
After generation:
- wrappers extend contract types
- no duplicate envelope classes exist
- generics are preserved
If true:
Client is correctly aligned
β οΈ Error handling
Errors are not generated models.
They follow a runtime protocol:
ProblemDetail (RFC 9457)
Example:
try {
client.call();
} catch (ApiProblemException ex) {
var pd = ex.getProblem();
}
π§ Mental model
Think of the client as:
A deterministic build-time compiler that maps OpenAPI β contract-aligned Java code
Not:
- a DTO generator
- a modeling tool
π§Ύ Summary
Input = OpenAPI
Process = controlled build pipeline
Output = thin wrappers over contract
The system works because:
- contract is never regenerated
- generation is deterministic
- boundaries are strictly enforced
π‘ MIT License