Skip to content

Latest commit

 

History

History
414 lines (286 loc) · 8.24 KB

File metadata and controls

414 lines (286 loc) · 8.24 KB
layout default
title Server-Side Adoption
parent Adoption Guides
nav_order 1
has_toc false

Server-Side Adoption — Contract-First OpenAPI Publication

Publish a deterministic, generics-aware OpenAPI from Spring Boot with one contract and zero duplication.

This is a contract-first projection system, not a documentation tool.

You define your contract in Java. The platform guarantees a stable, generator-ready OpenAPI projection.


Contents


60-second quick start

You want:

  • deterministic OpenAPI output
  • no envelope duplication
  • generics preserved in generated clients

Do this:

1) Add dependency

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

2) Return contract types

Default envelope:

ServiceResponse<CustomerDto>
ServiceResponse<Page<CustomerDto>>

Or bring your own envelope (BYOE):

ApiResponse<CustomerDto>

(Optional configuration for BYOE)

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

3) Expose OpenAPI

/v3/api-docs.yaml

Done.


What the server actually does

The server has one responsibility:

Project the runtime Java contract into a deterministic OpenAPI representation.

It does not:

  • generate clients
  • define alternative models
  • adapt output for specific generators

It only performs:

Java Contract → OpenAPI (projection)

Everything else happens downstream.


The only rule that matters

There is one canonical success model — but not a single fixed type.

YourEnvelope<T>

Supported shapes (deterministic scope):

ServiceResponse<T>
ServiceResponse<Page<T>>
YourEnvelope<T>

ServiceResponse<T> is the default envelope provided by the platform. BYOE (Bring Your Own Envelope) lets you substitute any envelope that meets the constraints.

Detailed constraints and violation examples are defined in Rules (do NOT break these).


Minimal dependencies

No configuration required.

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

Requirements:

  • Spring Boot (WebMVC)
  • OpenAPI endpoint (/v3/api-docs) via springdoc

What you actually write

You write only your domain contract.

Default (platform-provided envelope)

@GetMapping("/{id}")
public ResponseEntity<ServiceResponse<CustomerDto>> getCustomer(...) {
  return ResponseEntity.ok(ServiceResponse.of(dto));
}

Pagination (default envelope)

@GetMapping
public ResponseEntity<ServiceResponse<Page<CustomerDto>>> getCustomers(...) {
  return ResponseEntity.ok(ServiceResponse.of(page));
}

Using your own envelope (BYOE)

@GetMapping("/{id}")
public ResponseEntity<ApiResponse<CustomerDto>> getCustomer(...) {
  return ResponseEntity.ok(ApiResponse.success(dto));
}

Configuration:

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

No annotations. No schema config. No wrapper DTOs. The platform detects the envelope and projects it into OpenAPI deterministically.

Note on payload flexibility

The system constrains envelope structure — not payload content. You can place any domain type inside your envelope:

ServiceResponse<CustomerDto>
YourEnvelope<Anything>

What gets published to OpenAPI

From:

ServiceResponse<CustomerDto>

or (BYOE):

ApiResponse<CustomerDto>

The system produces a deterministic wrapper schema:

ServiceResponseCustomerDto
ApiResponseCustomerDto

Characteristics:

  • deterministic naming (based on envelope + payload type)
  • allOf composition
  • vendor extensions for generation

Example extensions:

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

Notes:

  • The envelope itself is NOT generated — it is treated as an external contract
  • Wrapper schemas exist only to bind OpenAPI responses to the generic contract shape
  • The same mechanism applies to both default and custom envelopes

OpenAPI is a projection artifact, not the source of truth.


Projection pipeline (what really happens)

The server is not "configurable". It is a fixed execution pipeline.

Controller return types
        ↓
Response type discovery
        ↓
Contract-aware introspection (envelope + payload)
        ↓
Base schema registration
        ↓
Wrapper schema generation
        ↓
Container enrichment
        ↓
Duplicate model marking
        ↓
Contract validation (fail-fast)
        ↓
OpenAPI output

The pipeline is envelope-agnostic — the same stages run for the default ServiceResponse<T> and for BYOE-configured external envelopes. The only difference is envelope resolution at the introspection stage; all downstream logic remains identical.

Key properties

  • single orchestrator (no ordering issues)
  • no patching / no overrides
  • deterministic output
  • envelope-agnostic (default or BYOE uses the same pipeline)

Rules (do NOT break these)

These are hard architectural constraints.

Rule 1: Use a single generic envelope model

ServiceResponse<T>
ServiceResponse<Page<T>>
YourEnvelope<T>

Constraints:

  • exactly one direct generic payload must exist
  • nested generic payloads in custom envelopes are not supported

Rule 2: You MAY use your own envelope (BYOE)

Valid:

ApiResponse<T>
CustomEnvelope<T>

Conditions:

  • must be a concrete class
  • must declare exactly one type parameter
  • must contain exactly one direct payload field of type T

Violations — these fail fast at application startup with a clear error message:

ApiResponse<Page<T>>      ❌ nested generic payload
ApiResponse<T, M>         ❌ multiple type parameters
WrapperWithoutPayload     ❌ no payload field
InterfaceEnvelope<T>      ❌ not a concrete class
RecordEnvelope<T>         ❌ records are not supported
AbstractEnvelope<T>       ❌ not a concrete class

Rule 3: Error model is NOT enforced by the platform

Error handling is not dictated by the starter. It depends entirely on the contract model defined by the service.

Two common patterns:

Pattern A — Separate error protocol (recommended default):

Success → ServiceResponse<T>
Error   → ProblemDetail (RFC 9457)

Pattern B — Envelope-based error model (custom / BYOE):

Success → YourEnvelope<T>
Error   → YourEnvelope<T> (e.g. errors field)

Key point: The platform enforces success envelope structure, but does not define or restrict error semantics.

Rule 4: Do NOT customize OpenAPI

No:

  • manual schemas (when using Springdoc mode)
  • annotations for schema shaping
  • overrides of generated structure

The starter owns projection.


Quick verification

curl http://localhost:8084/.../v1/.../1

Default envelope (ServiceResponse):

{
  "data": { ... },
  "meta": { ... }
}

Custom envelope (BYOE):

{
  "data": { ... },
  "status": 200,
  "message": "OK"
}

Key check:

  • exactly one payload field carries the response data
  • envelope structure matches your contract (default or custom)

If correct:

Server → OpenAPI → Client is aligned

Samples

Full end-to-end examples are provided:

  • Spring Boot 3 samples
  • Spring Boot 4 samples
  • client generation examples
  • consumer services

These demonstrate:

  • how the contract is produced
  • how the client is generated
  • how it is consumed safely

If anything is unclear, inspect samples instead of guessing.