You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TL;DR: Move from duplicated wrappers to auto-registered envelopes + thin, generics-aware clients — with a clear path to publishing as reusable artifacts.
Context
This project implements end-to-end generics-aware OpenAPI client generation. It aims to prove that full type-safety and envelope consistency can be achieved without configuration, using reflection alone.
One critical design area is how to detect and register generic response envelopes (ServiceResponse<T>, ServiceResponse<Page<T>>, etc.). Previous discussions considered configuration-driven allow-lists and optional annotations, but the consensus is now clear: keep it simple and fully reflective.
🔄 Current Direction — Reflection-Only Auto Detection
We adopt a KISS, zero-config, reflection-first approach.
Controller Inspection
The system introspects Spring controllers’ return types.
It unwraps layers like ResponseEntity, Mono, DeferredResult, etc., until it reaches a ServiceResponse<...>.
Root & Nested Generics Resolution
Extracts the root type (e.g., ServiceResponse) and the nested type(s) using ResolvableType.
Supports up to two levels of generic depth — e.g., ServiceResponse<T> and ServiceResponse<Page<T>>.
This covers 95% of real-world REST APIs (single resource + paginated resource).
Nested Container Recognition
Detects known containers such as Page<T> automatically.
For unknown or complex generics, gracefully degrades by omitting x-data-container/x-data-item vendor extensions (the client still builds correctly, only without nested context).
Unknown wrappers default to a safe fallback schema (generic object).
No need for any YAML or marker-based configuration.
🔍 Why Not Configuration or Annotations?
Not needed in this context: The reflection-based model already captures all controller-level envelopes deterministically.
KISS alignment: Configuration introduces friction; this project’s role is to prove that full automation is achievable.
Focus: A single, predictable behavior ensures consistent client generation and reproducible OpenAPI specs.
There will be no configuration files, allow-lists, or annotations. Reflection is deterministic and self-sufficient for discovering envelope hierarchies.
🔄 Generic Depth Policy
Level
Example
Support
Notes
1
ServiceResponse<CustomerDto>
✅
Standard envelope
2
ServiceResponse<Page<CustomerDto>>
✅
Covers paginated/nested results
3+
ServiceResponse<Page<Map<K,V>>>
🚫 (future)
Rare in practice; can be added later
Rationale: Two layers are enough for 99% of REST use cases. Beyond that, OpenAPI readability and template complexity degrade significantly.
🔧 How This Works Internally
This logic ensures that every controller method with a ServiceResponse<?> return type is mapped into a deterministic OpenAPI schema — no surprises, no duplication.
Uses ResolvableType recursively to resolve generics.
Unwraps known async/sync containers:
ResponseEntity<T> → T
CompletionStage<T> / Future<T> → T
DeferredResult<T> / WebAsyncTask<T> → T
Mono<T> / Flux<T> → T
Stops once it reaches a ServiceResponse or depth limit.
If resolved type = ServiceResponse<Page<CustomerDto>>, vendor extensions are added:
Reflection reliably discovers the nested generic (Container<T>). We do not add container allow-lists, YAML keys, or annotations. The only customization is which base class name to extend in generated clients.
🛠 Roadmap (Post-Validation Phase)
After validating the reflection-first approach in production use, minimal evolution steps may include:
Pluginization (optional packaging)
Package current reflection logic as reusable artifacts:
Reflectionless environments may add a small opt-in adapter in a separate module if truly needed.
🌟 Summary
This repository demonstrates that fully automated, type-safe OpenAPI generation can be achieved with zero configuration. Reflection is the contract. Two-level generics are the upper bound. No allow-lists, no annotations, no YAML switches — just predictable, reflection-driven envelopes.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Context
This project implements end-to-end generics-aware OpenAPI client generation. It aims to prove that full type-safety and envelope consistency can be achieved without configuration, using reflection alone.
One critical design area is how to detect and register generic response envelopes (
ServiceResponse<T>
,ServiceResponse<Page<T>>
, etc.). Previous discussions considered configuration-driven allow-lists and optional annotations, but the consensus is now clear: keep it simple and fully reflective.🔄 Current Direction — Reflection-Only Auto Detection
We adopt a KISS, zero-config, reflection-first approach.
Controller Inspection
ResponseEntity
,Mono
,DeferredResult
, etc., until it reaches aServiceResponse<...>
.Root & Nested Generics Resolution
ServiceResponse
) and the nested type(s) usingResolvableType
.ServiceResponse<T>
andServiceResponse<Page<T>>
.Nested Container Recognition
Page<T>
automatically.x-data-container
/x-data-item
vendor extensions (the client still builds correctly, only without nested context).Safety and Predictability
MAX_UNWRAP_DEPTH = 8
) prevents infinite recursion.object
).🔍 Why Not Configuration or Annotations?
🔄 Generic Depth Policy
ServiceResponse<CustomerDto>
ServiceResponse<Page<CustomerDto>>
ServiceResponse<Page<Map<K,V>>>
Rationale: Two layers are enough for 99% of REST use cases. Beyond that, OpenAPI readability and template complexity degrade significantly.
🔧 How This Works Internally
This logic ensures that every controller method with a
ServiceResponse<?>
return type is mapped into a deterministic OpenAPI schema — no surprises, no duplication.Uses
ResolvableType
recursively to resolve generics.Unwraps known async/sync containers:
ResponseEntity<T>
→T
CompletionStage<T>
/Future<T>
→T
DeferredResult<T>
/WebAsyncTask<T>
→T
Mono<T>
/Flux<T>
→T
Stops once it reaches a
ServiceResponse
or depth limit.If resolved type =
ServiceResponse<Page<CustomerDto>>
, vendor extensions are added:If container or inner type cannot be determined, extensions are omitted safely.
🔄 Error Tolerance & Logging
🔝 Guiding Principles
🧩 Generator Option —
envelopeBaseClass
The OpenAPI document never carries the envelope class name. Instead, the client build selects a single base class via a small generator knob:
envelopeBaseClass
— simple name of the generic client base (default:ServiceClientResponse
).commonPackage
— package where the base class lives (already in use).This keeps the spec independent of framework naming while letting each consumer team pick their own base class if they really need to.
Maven example:
Mustache overlay:
🛠 Roadmap (Post-Validation Phase)
After validating the reflection-first approach in production use, minimal evolution steps may include:
Pluginization (optional packaging)
Package current reflection logic as reusable artifacts:
openapi-generics-autoreg
(server, Springdoc customizer)openapi-generics-templates
(client overlay)Non-Spring environments (deferred)
🌟 Summary
This repository demonstrates that fully automated, type-safe OpenAPI generation can be achieved with zero configuration. Reflection is the contract. Two-level generics are the upper bound. No allow-lists, no annotations, no YAML switches — just predictable, reflection-driven envelopes.
Beta Was this translation helpful? Give feedback.
All reactions