Skip to content

Commit a5a3ed2

Browse files
committed
fix(client): improve error handling, documentation, and response models
- Fixed RestClient defaultStatusHandler to no longer swallow exceptions; all non-2xx responses now raise ClientProblemException with parsed or fallback ProblemDetail. - Refactored ClientProblemException to produce structured, readable messages and include RFC 9457 fields (type, instance, errorCode). - Enhanced ProblemDetailSupport to safely parse or fallback on malformed bodies. - Added Javadoc and helper factory methods to Meta (now(), with Sort, SortField, SortDirection) for easier creation and future-proof extensibility. - Added Javadoc to ProblemExtensions to clarify its purpose as a flexible container for RFC 9457 extensions (e.g., traceId, pagination, metadata). - Updated README and documentation examples to reflect corrected handler behavior. - Verified behavior with MockWebServer and RestClient integration tests. This completes the 0.7.1 stabilization — behavior is now consistent, explicit, and fully RFC 9457 compliant.
1 parent c021e69 commit a5a3ed2

File tree

12 files changed

+349
-324
lines changed

12 files changed

+349
-324
lines changed

README.md

Lines changed: 66 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@
1313
<p align="center">
1414
<img src="docs/images/openapi-generics-cover.png" alt="OpenAPI Generics Cover" width="720"/>
1515
<br/>
16-
<em>End-to-end generics-aware OpenAPI clients — unified <code>{ data, meta }</code> responses without boilerplate.</em>
16+
<em><strong>End-to-end generics-aware OpenAPI clients</strong> — unified <code>{ data, meta }</code> responses without boilerplate.</em>
1717
</p>
1818

1919
**Modern, type-safe OpenAPI client generation** — powered by **Spring Boot 3.4**, **Java 21**, and **OpenAPI Generator 7.16.0**.
20-
This repository demonstrates a production-grade architecture where backend and client are fully aligned through generics, enabling nested generic envelopes (`ServiceResponse<Page<T>>`) and [**RFC 7807 — Problem Details for HTTP APIs**](https://datatracker.ietf.org/doc/html/rfc7807)-based error handling.
20+
This repository demonstrates a production-grade architecture where backend and client are fully aligned through generics, enabling nested generic envelopes (`ServiceResponse<Page<T>>`) and [**RFC 9457 — Problem Details for HTTP APIs**](https://www.rfc-editor.org/rfc/rfc9457)-based error handling.
21+
22+
> 🧠 **RFC 9457 vs RFC 7807**
23+
> RFC 9457 supersedes 7807 and standardizes `application/problem+json` / `application/problem+xml` for HTTP APIs.
24+
> Spring Framework 6+ implements this via the built-in `ProblemDetail` class, enabling consistent error serialization across server and client.
2125
2226
---
2327

@@ -36,41 +40,45 @@ This repository demonstrates a production-grade architecture where backend and c
3640
* 📘 [Adoption Guides](#-adoption-guides)
3741
* 🔗 [References & External Links](#-references--external-links)
3842

39-
> *A clean architecture pattern for building generics-aware OpenAPI clients that stay fully type-safe, consistent, and
40-
boilerplate-free.*
43+
> *A clean architecture pattern for building generics-aware OpenAPI clients that stay fully type-safe, consistent, and boilerplate-free.*
4144
4245
---
4346

4447
## 📦 Modules
4548

4649
* [**customer-service**](customer-service/README.md) — sample backend exposing `/v3/api-docs.yaml` via Springdoc
47-
* [**customer-service-client**](customer-service-client/README.md) — generated OpenAPI client with generics-aware
48-
wrappers
50+
* [**customer-service-client**](customer-service-client/README.md) — generated OpenAPI client with generics-aware wrappers
4951

5052
---
5153

5254
## 🚀 Problem & Motivation
5355

5456
OpenAPI Generator, by default, does not handle **generic response types**.
5557

56-
When backend APIs wrap payloads in `ServiceResponse<T>` (e.g., the unified `{ data, meta }` envelope),
57-
the generator produces **duplicated models per endpoint** instead of a single reusable generic base.
58+
When backend APIs wrap payloads in `ServiceResponse<T>` (e.g., the unified `{ data, meta }` envelope), the generator produces **duplicated models per endpoint** instead of a single reusable generic base.
5859

5960
This results in:
6061

6162
* ❌ Dozens of almost-identical response classes
6263
* ❌ Higher maintenance overhead
6364
* ❌ Harder to evolve a single envelope contract across services
6465

66+
```java
67+
// Default OpenAPI output (before)
68+
class CreateCustomerResponse { CustomerDto data; Meta meta; }
69+
class UpdateCustomerResponse { CustomerDto data; Meta meta; }
70+
// ... dozens of duplicates
71+
```
72+
6573
---
6674

6775
## 💡 Solution Overview
6876

69-
This project provides a **full-stack pattern** to align Spring Boot services and OpenAPI clients.
77+
This project provides a **full-stack pattern** aligning Spring Boot services and OpenAPI clients through automatic schema introspection and template overlay.
7078

71-
### Server-Side (Producer)
79+
### 🖥️ Server-Side (Producer)
7280

73-
A `Springdoc` customizer automatically inspects controller return types like:
81+
A `Springdoc` customizer inspects controller return types such as:
7482

7583
```java
7684
ResponseEntity<ServiceResponse<CustomerDto>>
@@ -79,71 +87,69 @@ ResponseEntity<ServiceResponse<Page<CustomerDto>>>
7987

8088
and enriches the generated OpenAPI schema with vendor extensions:
8189

82-
**For single type (`ServiceResponse<T>`):**
90+
**Single type (`ServiceResponse<T>`):**
8391

8492
```yaml
85-
# For simple generic responses
8693
x-api-wrapper: true
8794
x-api-wrapper-datatype: CustomerDto
8895
```
8996
90-
**For nested generics (`ServiceResponse<Page<T>>`):**
97+
**Nested generics (`ServiceResponse<Page<T>>`):**
9198

9299
```yaml
93100
x-api-wrapper: true
94101
x-data-container: Page
95102
x-data-item: CustomerDto
96103
```
97104

98-
These hints make the OpenAPI spec fully aware of generic and nested structures — no manual annotations required.
99-
100-
---
105+
These extensions make the OpenAPI spec *aware* of generic and nested structures — no manual annotations required.
101106

102-
### Client-Side (Consumer)
107+
### 💻 Client-Side (Consumer)
103108

104-
Mustache overlays redefine OpenAPI templates to generate **thin, type-safe wrappers** extending a reusable base class `ServiceClientResponse<T>`.
109+
Custom Mustache overlays redefine OpenAPI templates to generate **thin, type-safe wrappers** extending the reusable base `ServiceClientResponse<T>`.
105110

106-
**Example generated output:**
111+
**Generated output:**
107112

108113
```java
109114
// Single
110-
public class ServiceResponseCustomerDto
115+
data class ServiceResponseCustomerDto
111116
extends ServiceClientResponse<CustomerDto> {}
112117
113118
// Paged
114-
public class ServiceResponsePageCustomerDto
119+
data class ServiceResponsePageCustomerDto
115120
extends ServiceClientResponse<Page<CustomerDto>> {}
116121
```
117122

118-
This pattern supports **nested generics** like `ServiceClientResponse<Page<CustomerDto>>` and automatically maps all error responses into **ProblemDetail** objects.
123+
✅ Supports **nested generics** like `ServiceClientResponse<Page<CustomerDto>>`
124+
✅ Automatically maps error responses into **RFC 9457 Problem Details**
119125

120126
---
121127

122-
## &#9881; Architecture Overview
128+
## ⚙️ Architecture Overview
123129

124130
<p align="center">
125131
<img src="docs/images/architectural-diagram.png" alt="OpenAPI Generics Architecture" width="900"/>
126132
<br/>
127133
<em>End-to-end generics-aware architecture: from Spring Boot producer to OpenAPI client consumer.</em>
128134
</p>
129135

130-
| Layer | Description |
131-
|-----------------------|---------------------------------------------------------------------------|
132-
| **Server (Producer)** | Publishes an **OpenAPI 3.1-compliant** specification via Springdoc 2.8.13 with auto-registered wrapper schemas |
133-
| **Client (Consumer)** | Uses **OpenAPI Generator 7.16.0** with custom Mustache overlays for generics support |
134-
| **Envelope Model** | Unified `{ data, meta }` response structure |
135-
| **Error Handling** | RFC 7807-compliant `ProblemDetail` decoding into `ClientProblemException` |
136-
| **Nested Generics** | Full support for `ServiceResponse<Page<T>>` |
136+
| Layer | Description |
137+
| --------------------- | ----------------------------------------------------------------------------------------------------- |
138+
| **Server (Producer)** | Publishes an **OpenAPI 3.1-compliant** spec via Springdoc 2.8.13 with auto-registered wrapper schemas |
139+
| **Client (Consumer)** | Uses **OpenAPI Generator 7.16.0** with Mustache overlays for generics support |
140+
| **Envelope Model** | Unified `{ data, meta }` response structure |
141+
| **Error Handling** | **RFC 9457-compliant Problem Details** decoded into `ClientProblemException` |
142+
| **Nested Generics** | Full support for `ServiceResponse<Page<T>>` |
137143

138144
---
139145

140146
## ⚡ Quick Start
141147

142148
```bash
143-
# Run the backend service
149+
# Run backend service
144150
cd customer-service && mvn spring-boot:run
145151
146-
# Generate and build the OpenAPI client
152+
# Generate and build client
147153
cd ../customer-service-client && mvn clean install
148154
```
149155

@@ -153,37 +159,35 @@ Generated wrappers appear under:
153159
target/generated-sources/openapi/src/gen/java
154160
```
155161

156-
Each wrapper extends `ServiceClientResponse<T>` and aligns perfectly with the `{ data, meta }` envelope model.
162+
Each wrapper extends `ServiceClientResponse<T>` and aligns with the unified `{ data, meta }` envelope.
157163

158-
You can now test end-to-end type-safe responses through the generated client — verifying both single and paged envelopes in action.
164+
Now you can test end-to-end type-safe responses via the generated client — validating both single and paged envelopes.
159165

160166
---
161167

162168
## 🔄 Generated Wrappers — Before & After
163169

164-
Comparison of how OpenAPI Generator outputs looked **before** vs **after** enabling the generics-aware wrapper support.
165-
166-
**Before (duplicated full model):**
170+
**Before (duplicated full models):**
167171

168172
<p align="center">
169173
<img src="docs/images/generated-client-wrapper-before.png" alt="Generated client before generics support" width="800"/>
170174
<br/>
171-
<em>Each endpoint generated its own full response model — duplicated <code>data</code> and <code>meta</code> fields across classes.</em>
175+
<em>Each endpoint generated its own response class, duplicating <code>data</code> and <code>meta</code> fields.</em>
172176
</p>
173177

174178
**After (thin generic wrapper):**
175179

176180
<p align="center">
177181
<img src="docs/images/generated-client-wrapper-after.png" alt="Generated client after generics support" width="800"/>
178182
<br/>
179-
<em>Now every endpoint extends the reusable <code>ServiceClientResponse&lt;Page&lt;T&gt;&gt;</code> base, eliminating boilerplate and preserving type safety.</em>
183+
<em>Each endpoint now extends the reusable <code>ServiceClientResponse&lt;Page&lt;T&gt;&gt;</code> base, eliminating boilerplate and preserving type safety.</em>
180184
</p>
181185

182-
---
186+
---
183187

184188
## 🧱 Example Responses
185189

186-
The unified envelope applies to both single and paged responses.
190+
Unified envelope structure applies to both single and paged results.
187191

188192
### 🧩 Single Item Example (`ServiceClientResponse<CustomerDto>`)
189193

@@ -207,16 +211,8 @@ The unified envelope applies to both single and paged responses.
207211
{
208212
"data": {
209213
"content": [
210-
{
211-
"customerId": 1,
212-
"name": "Jane Doe",
213-
"email": "[email protected]"
214-
},
215-
{
216-
"customerId": 2,
217-
"name": "John Smith",
218-
"email": "[email protected]"
219-
}
214+
{ "customerId": 1, "name": "Jane Doe", "email": "[email protected]" },
215+
{ "customerId": 2, "name": "John Smith", "email": "[email protected]" }
220216
],
221217
"page": 0,
222218
"size": 5,
@@ -227,24 +223,19 @@ The unified envelope applies to both single and paged responses.
227223
},
228224
"meta": {
229225
"serverTime": "2025-01-01T12:34:56Z",
230-
"sort": [
231-
{
232-
"field": "CUSTOMER_ID",
233-
"direction": "ASC"
234-
}
235-
]
226+
"sort": [ { "field": "CUSTOMER_ID", "direction": "ASC" } ]
236227
}
237228
}
238229
```
239-
Both examples demonstrate the unified { data, meta } structure —
240-
the same envelope applies seamlessly across single and paged results.
230+
231+
> **Content-Type:** `application/json` (success)
232+
> **Content-Type:** `application/problem+json` (error — RFC 9457)
241233

242234
### Client Usage
243235

244236
```java
245237
ServiceClientResponse<Page<CustomerDto>> resp =
246-
customerClientAdapter.getCustomers(
247-
"Jane", null, 0, 5, SortField.CUSTOMER_ID, SortDirection.ASC);
238+
customerClientAdapter.getCustomers("Jane", null, 0, 5, SortField.CUSTOMER_ID, SortDirection.ASC);
248239
249240
Page<CustomerDto> page = resp.getData();
250241
for (CustomerDto c : page.content()) {
@@ -257,7 +248,7 @@ for (CustomerDto c : page.content()) {
257248
## 🧩 Tech Stack
258249

259250
| Component | Version | Purpose |
260-
|-----------------------|---------|---------------------------------------|
251+
| --------------------- | ------- | ------------------------------------- |
261252
| **Java** | 21 | Language baseline |
262253
| **Spring Boot** | 3.4.10 | REST + OpenAPI provider |
263254
| **Springdoc** | 2.8.13 | OpenAPI 3.1 integration |
@@ -270,9 +261,9 @@ for (CustomerDto c : page.content()) {
270261

271262
* 🔹 Unified `{ data, meta }` response model
272263
* 🔹 Nested generics support — `ServiceResponse<Page<T>>`
273-
* 🔹 RFC 7807-compliant error mapping (`ProblemDetail`)
274-
* 🔹 Mustache overlay templates for thin wrapper generation
275-
* 🔹 Seamless compatibility between backend and client
264+
* 🔹 **RFC 9457-compliant Problem Details** (`application/problem+json`)
265+
* 🔹 Mustache overlays for thin wrapper generation
266+
* 🔹 Full alignment between producer and consumer
276267
* 🔹 Zero boilerplate — clean, evolvable, and type-safe
277268

278269
---
@@ -282,20 +273,18 @@ for (CustomerDto c : page.content()) {
282273
```java
283274
public interface CustomerClientAdapter {
284275
ServiceClientResponse<CustomerDto> createCustomer(CustomerCreateRequest request);
285-
286276
ServiceClientResponse<CustomerDto> getCustomer(Integer customerId);
287-
288277
ServiceClientResponse<Page<CustomerDto>> getCustomers();
289278
}
290279
```
291280

292-
This adapter defines a stable contract that hides generated artifacts and provides type-safe access to your APIs.
281+
A stable adapter contract hides generated artifacts while preserving strong typing and client independence.
293282

294283
---
295284

296285
## 📘 Adoption Guides
297286

298-
Explore integration steps under [`docs/adoption`](docs/adoption):
287+
See integration details under [`docs/adoption`](docs/adoption):
299288

300289
* [Server-Side Adoption](docs/adoption/server-side-adoption.md)
301290
* [Client-Side Adoption](docs/adoption/client-side-adoption.md)
@@ -305,8 +294,10 @@ Explore integration steps under [`docs/adoption`](docs/adoption):
305294
## 🔗 References & External Links
306295

307296
* 🌐 [GitHub Repository](https://github.com/bsayli/spring-boot-openapi-generics-clients)
308-
* 📘 [Medium — We Made OpenAPI Generator Think in Generics](https://medium.com/@baris.sayli/type-safe-generic-api-responses-with-spring-boot-3-4-openapi-generator-and-custom-templates-ccd93405fb04)
297+
* 📰 [Medium — We Made OpenAPI Generator Think in Generics](https://medium.com/@baris.sayli/type-safe-generic-api-responses-with-spring-boot-3-4-openapi-generator-and-custom-templates-ccd93405fb04)
309298
* 💬 [Dev.to — We Made OpenAPI Generator Think in Generics](https://dev.to/barissayli/spring-boot-openapi-generator-type-safe-generic-api-clients-without-boilerplate-3a8f)
299+
* 📘 [RFC 9457 — Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457)
300+
310301
---
311302

312303
## 🛡 License
@@ -317,14 +308,14 @@ Licensed under **MIT** — see [LICENSE](LICENSE).
317308

318309
## 💬 Feedback
319310

320-
If you spot an error or have suggestions, open an issue or join the discussion — contributions are welcome.
311+
If you spot an error or have suggestions, open an issue or join the discussion — contributions are welcome.
321312
💭 [Start a discussion →](https://github.com/bsayli/spring-boot-openapi-generics-clients/discussions)
322313

323314
---
324315

325316
## 🤝 Contributing
326317

327-
Contributions, issues, and feature requests are welcome!
318+
Contributions, issues, and feature requests are welcome!
328319
Feel free to [open an issue](https://github.com/bsayli/spring-boot-openapi-generics-clients/issues) or submit a PR.
329320

330321
---
@@ -335,5 +326,5 @@ If you found this project helpful, please give it a ⭐ on GitHub — it helps o
335326

336327
---
337328

338-
**Barış Saylı**
339-
[GitHub](https://github.com/bsayli) · [Medium](https://medium.com/@baris.sayli) · [LinkedIn](https://www.linkedin.com/in/bsayli)
329+
**Barış Saylı**
330+
[GitHub](https://github.com/bsayli) · [Medium](https://medium.com/@baris.sayli) · [LinkedIn](https://www.linkedin.com/in/bsayli)

0 commit comments

Comments
 (0)