Skip to content

Commit b4264e9

Browse files
committed
# spring-boot-openapi-generics-clients
[![Build](https://github.com/bsayli/spring-boot-openapi-generics-clients/actions/workflows/build.yml/badge.svg)](https://github.com/bsayli/spring-boot-openapi-generics-clients/actions/workflows/build.yml) [![Release](https://img.shields.io/github/v/release/bsayli/spring-boot-openapi-generics-clients?logo=github&label=release)](https://github.com/bsayli/spring-boot-openapi-generics-clients/releases/latest) [![codecov](https://codecov.io/gh/bsayli/spring-boot-openapi-generics-clients/branch/main/graph/badge.svg)](https://codecov.io/gh/bsayli/spring-boot-openapi-generics-clients) [![Java](https://img.shields.io/badge/Java-21-red?logo=openjdk)](https://openjdk.org/projects/jdk/21/) [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4.10-green?logo=springboot)](https://spring.io/projects/spring-boot) [![OpenAPI Generator](https://img.shields.io/badge/OpenAPI%20Generator-7.16.0-blue?logo=openapiinitiative)](https://openapi-generator.tech/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) --- <p align="center"> <img src="docs/images/social-preview.png" alt="Social preview" width="720"/> <br/> <em>Type-safe API responses without boilerplate — powered by Spring Boot & OpenAPI Generator</em> </p> **Type-safe client generation with Spring Boot & OpenAPI using generics.** This repository demonstrates how to extend OpenAPI Generator to work with generics in order to avoid boilerplate, reduce duplicated wrappers, and keep client code clean. --- ## 📑 Table of Contents - 📦 [Modules](#-modules-in-this-repository) - 🛠 [Compatibility Matrix](#-compatibility-matrix) - 🚀 [Problem Statement](#-problem-statement) - 💡 [Solution](#-solution) - ⚡ [Quick Start](#-quick-start) - 🧩 [Tech Stack](#-tech-stack--features) - ✅ [Key Features](#-key-features) - ✨ [Usage Example](#-usage-example-adapter-interface) - 📦 [Related Modules](#-related-modules-quick-view) - 📘 [Adoption Guides](#-adoption-guides) - 🔗 [References & Links](#-references--links) ### 📦 Modules in this Repository This repository consists of two main modules: - [**customer-service**](customer-service/README.md) — Sample API producer (Spring Boot microservice + OpenAPI spec) - [**customer-service-client**](customer-service-client/README.md) — Generated Java client (generics support via custom templates) --- ### 🔧 Compatibility Matrix | Component | Version | |-------------------------|---------| | **Java** | 21 | | **Spring Boot** | 3.4.10 | | **Springdoc OpenAPI** | 2.8.13 | | **OpenAPI Generator** | 7.16.0 | | **Apache HttpClient 5** | 5.5 | --- ## 🚀 Problem Statement Most backend teams standardize responses with a generic wrapper like `ServiceResponse<T>`. However, **OpenAPI Generator does not natively support generics** — instead, it generates one wrapper per endpoint ( duplicating fields like `status`, `message`, and `errors`). This creates: * ❌ Dozens of almost-identical classes * ❌ High maintenance overhead * ❌ No single place to evolve the response envelope --- ## 💡 Solution This project shows how to: * Customize **Springdoc** to mark wrapper schemas in OpenAPI * Add a **tiny Mustache partial** so the generator emits thin shells extending a reusable generic base * Keep **compile-time type safety** without repetitive mappers --- ### How it works (under the hood) At generation time, the reference service **auto-registers** wrapper schemas in the OpenAPI doc: * A Spring `OpenApiCustomizer` scans controller return types and unwraps `ResponseEntity`, `CompletionStage`, Reactor ( `Mono`/`Flux`), etc. until it reaches `ServiceResponse<T>`. * For every discovered `T`, it adds a `ServiceResponse{T}` schema that composes the base envelope + the concrete `data` type, and marks it with vendor extensions: * `x-api-wrapper: true` * `x-api-wrapper-datatype: <T>` The Java client then uses a tiny Mustache override to render **thin shells** for those marked schemas: ```mustache // api_wrapper.mustache public class {{classname}} extends io.github.bsayli.openapi.client.common.ServiceClientResponse<{{vendorExtensions.x-api-wrapper-datatype}}> { } ``` This is what turns e.g. `ServiceResponseCustomerCreateResponse` into: ```java public class ServiceResponseCustomerCreateResponse extends ServiceClientResponse<CustomerCreateResponse> { } ``` --- ## ⚡ Quick Start Run the reference service: ```bash cd customer-service mvn spring-boot:run ``` Generate and build the client: ```bash cd customer-service-client mvn clean install ``` Use the generated API: ```java ServiceClientResponse<CustomerCreateResponse> response = customerControllerApi.createCustomer(request); ``` ### 🖼 Swagger Screenshot Here’s what the `create customer` endpoint looks like in Swagger UI after running the service: ![Customer create example](docs/images/swagger-customer-create.png) ### 🖼 Generated Client Wrapper Comparison of how OpenAPI Generator outputs looked **before** vs **after** adding the generics-aware wrapper: **Before (duplicated full model):** ![Generated client (before)](docs/images/generated-client-wrapper-before.png) **After (thin generic wrapper):** ![Generated client (after)](docs/images/generated-client-wrapper-after.png) --- ## ✅ Verify in 60 Seconds 1. Clone this repo 2. Run `mvn clean install -q` 3. Open `customer-service-client/target/generated-sources/...` 4. See the generated wrappers → they now extend a **generic base class** instead of duplicating fields. You don’t need to write a single line of code — the generator does the work. --- ## 🛠 Tech Stack & Features * 🚀 **Java 21** — modern language features * 🍃 **Spring Boot 3.4.10** — microservice foundation * 📖 **Springdoc OpenAPI** — API documentation * 🔧 **OpenAPI Generator 7.x** — client code generation * 🧩 **Custom Mustache templates** — generics-aware wrappers * 🧪 **JUnit 5 + MockWebServer** — integration testing * 🌐 **Apache HttpClient 5** — connection pooling & timeouts --- ## 📦 Next Steps: Dependency-based Adoption The long-term goal is to publish the core pieces as standalone modules, so that any project using a generic response type like `ServiceResponse<T>` can enable the same behavior with **just one dependency**: - `io.github.bsayli:openapi-generics-autoreg` → **server-side**: automatically registers wrapper schemas in the OpenAPI spec. - `io.github.bsayli:openapi-generics-templates` → **client-side**: plugs into OpenAPI Generator for thin, type-safe wrappers. This will let teams adopt **generics-aware OpenAPI support** without copying customizers or Mustache templates — just by adding a Maven/Gradle dependency. --- ## 📂 Project Structure ```text spring-boot-openapi-generics-clients/ ├── customer-service/ # Sample Spring Boot microservice (API producer) ├── customer-service-client/ # Generated client using custom templates └── README.md # Root documentation ``` --- ## 🧩 Key Features * ✅ **Generic base model**: `ServiceClientResponse<T>` * ✅ **Thin wrappers**: endpoint-specific shells extending the base * ✅ **Strong typing preserved**: `getData()` returns the exact payload type * ✅ **No duplicated fields** across wrappers * ✅ Easy to maintain and evolve --- ### ✨ Usage Example: Adapter Interface Sometimes you don’t want to expose all the thin wrappers directly. A simple adapter interface can consolidate them into clean, type-safe methods: ```java public interface CustomerClientAdapter { ServiceClientResponse<CustomerCreateResponse> createCustomer(CustomerCreateRequest request); ServiceClientResponse<CustomerDto> getCustomer(Integer customerId); ServiceClientResponse<CustomerListResponse> getCustomers(); ServiceClientResponse<CustomerUpdateResponse> updateCustomer( Integer customerId, CustomerUpdateRequest request); ServiceClientResponse<CustomerDeleteResponse> deleteCustomer(Integer customerId); } ``` --- ## 🔍 Why This Matters Without generics support, OpenAPI client generation creates bloated and repetitive code. By applying this approach: * Development teams **save time** maintaining response models * Client libraries become **cleaner and smaller** * Easier for **new developers** to understand the contract * Code stays **future-proof** when envelope fields evolve --- ## 💼 Use Cases This pattern is useful when: * You have **multiple microservices** with a shared response structure * You need to **evolve response envelopes** without breaking dozens of generated classes * You want **type safety** in generated clients but without boilerplate --- ## 🔧 How to Run 1. **Start the reference service** ```bash cd customer-service mvn spring-boot:run ``` 2. **Generate the client** ```bash cd customer-service-client mvn clean install ``` 3. **Use the generated API** ```java ServiceClientResponse<CustomerCreateResponse> response = customerControllerApi.createCustomer(request); ``` --- ## 👤 Who Should Use This? * Backend developers maintaining multiple microservices * API platform teams standardizing response envelopes * Teams already invested in OpenAPI Generator looking to reduce boilerplate --- ## ⚠️ Why Not Use It? This project may not be the right fit if: * Your APIs do **not** use a common response wrapper * You are fine with duplicated wrapper models * You don’t generate client code from OpenAPI specs --- ## 📂 References & Links - 📘 [Medium Article — Type-Safe Generic API Responses](https://medium.com/@baris.sayli/type-safe-generic-api-responses-with-spring-boot-3-4-openapi-generator-and-custom-templates-ccd93405fb04) - 🌐 [GitHub Pages (Adoption Guides)](https://bsayli.github.io/spring-boot-openapi-generics-clients/) --- ## 🛡 License MIT --- ✅ **Note:** CLI examples should always be provided **on a single line**. If parameters include spaces or special characters, wrap them in quotes `"..."`. --- ## 💬 Feedback If you spot any mistakes in this README or have questions about the project, feel free to open an issue or start a discussion. I’m happy to improve the documentation and clarify concepts further! --- ## 🤝 Contributing Contributions, issues, and feature requests are welcome! Feel free to [open an issue](../../issues) or submit a PR. --- ## ⭐ Support If you found this project useful, please consider giving it a star ⭐ on GitHub — it helps others discover it too! --- ## 📦 Related Modules (Quick View) | Module | Description | Docs | |--------------------------------|---------------------------------------------|---------------------------------------------| | 🟢 **customer-service** | Spring Boot sample API (producer) | [README](customer-service/README.md) | | 🔵 **customer-service-client** | Generated Java client with generics support | [README](customer-service-client/README.md) | --- ## 📘 Adoption Guides Looking to integrate this approach into your own project? See the detailed guides under [`docs/adoption`](docs/adoption): - [Server-Side Adoption](docs/adoption/server-side-adoption.md) - [Client-Side Adoption](docs/adoption/client-side-adoption.md)
1 parent 909a9ec commit b4264e9

File tree

3 files changed

+75
-31
lines changed

3 files changed

+75
-31
lines changed

customer-service-client/README.md

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -485,17 +485,40 @@ It enqueues responses for **all CRUD operations** and asserts correct mapping in
485485

486486
## 📚 Notes
487487

488-
* Dependencies like `spring-web`, `spring-context`, `jackson-*`, `jakarta.*` are marked **provided**; your host app
489-
supplies them.
490-
* Generator options: Spring 6 `RestClient`, Jakarta EE, Jackson, Java 21.
491-
* OpenAPI spec lives at: `src/main/resources/customer-api-docs.yaml`.
492-
If you re-run the server and want an updated client, re-pull the spec:
493-
488+
* **Provided (host app must supply):**
489+
- `org.springframework.boot:spring-boot-starter-web`
490+
- `org.springframework.boot:spring-boot-starter`
491+
- `jakarta.validation:jakarta.validation-api` ← required for generated model annotations (`@NotNull`, `@Size`, etc.)
492+
- `jakarta.annotation:jakarta.annotation-api`
493+
494+
These are marked as **provided** in the POM. Your host application must include them on the classpath.
495+
496+
* **Included runtime dependency:**
497+
- `org.apache.httpcomponents.client5:httpclient5`
498+
Required if you use **Option B (HttpClient5 pooling)**. Already declared as a normal dependency in the POM.
499+
500+
* **Generator & Toolchain:**
501+
- Java 21
502+
- OpenAPI Generator 7.16.0
503+
- Generator options: `useJakartaEe=true`, `serializationLibrary=jackson`, `dateLibrary=java8`,
504+
`useBeanValidation=true`
505+
- Note: `useBeanValidation=true` → generated code includes **Jakarta Validation** annotations (requires
506+
`jakarta.validation-api`).
507+
508+
* **Frameworks (host app / examples):**
509+
- Spring Boot 3.4.10
510+
- Jakarta Validation API 3.1.1
511+
- Apache HttpClient 5.5 (only needed if using Option B)
512+
513+
* **OpenAPI spec location:**
514+
`src/main/resources/customer-api-docs.yaml`
515+
516+
To refresh the spec after restarting the service:
494517
```bash
495518
curl -s http://localhost:8084/customer-service/v3/api-docs.yaml \
496519
-o src/main/resources/customer-api-docs.yaml
497520
mvn -q clean install
498-
```
521+
```
499522

500523
---
501524

@@ -529,7 +552,8 @@ public class ServiceResponseCustomerDeleteResponse
529552
```
530553

531554
By default this feature is **not required** and we recommend using the plain `ServiceClientResponse<T>` wrappers
532-
as-is. However, the hook is available if your project needs to enforce additional annotations (e.g., Jackson, Lombok)
555+
as-is. However, the hook is available if your project needs to enforce additional annotations (e.g., Jackson, Jakarta
556+
Validation)
533557
on top of generated wrapper classes.
534558

535559
---
@@ -538,14 +562,6 @@ on top of generated wrapper classes.
538562

539563
This repository is licensed under **MIT** (root `LICENSE`). Submodules inherit the license.
540564

541-
### Packaging note (optional)
542-
543-
This module is **reference-oriented**. If you want to publish it as a reusable library later:
544-
545-
* remove `provided` scopes and pin minimal runtime deps,
546-
* add a semantic version and release process (e.g., GitHub Release + `mvn deploy` to Maven Central),
547-
* keep the Mustache overlay in-repo for transparent builds.
548-
549565
---
550566

551567
## 📦 Related Module

customer-service/README.md

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,40 @@ against.
2626
## 📊 Architecture at a Glance
2727

2828
```
29-
Client
30-
31-
32-
Customer Service
33-
34-
35-
OpenAPI Spec (YAML/JSON)
36-
37-
38-
Customer Service Client
29+
[customer-service] ── publishes ──> /v3/api-docs.yaml (OpenAPI contract)
30+
31+
└─ consumed by OpenAPI Generator (+ generics-aware templates)
32+
33+
└─> [customer-service-client] (type-safe wrappers)
34+
35+
└─ used by consumer apps (your services)
3936
```
4037

41-
This module defines the contract; the client module consumes it.
38+
### Explanation
39+
40+
* **customer-service** exposes the OpenAPI contract at `/v3/api-docs.yaml` (and Swagger UI).
41+
* **customer-service-client** runs the OpenAPI Generator against that contract, applying generics-aware Mustache
42+
templates to produce **thin wrapper classes**.
43+
* **Your applications** then depend on this generated client. They call `CustomerControllerApi` (and other APIs)
44+
directly without worrying about HTTP details, connection management, or response parsing.
45+
46+
➡️ This separation keeps the **server-side contract** clear, the **client auto-generated**, and the **consumer apps
47+
strongly typed**.
48+
49+
---
50+
51+
## 🛠 Tech Stack
52+
53+
* **Java 21**
54+
* **Spring Boot 3.4.10**
55+
- spring-boot-starter-web
56+
- spring-boot-starter-validation
57+
- spring-boot-starter-test (test scope)
58+
* **OpenAPI / Swagger**
59+
- springdoc-openapi-starter-webmvc-ui (2.8.13)
60+
* **Build & Tools**
61+
- Maven 3.9+
62+
- JaCoCo, Surefire, Failsafe for test & coverage
4263

4364
---
4465

@@ -225,7 +246,6 @@ mvn test
225246
* OpenAPI spec (`/v3/api-docs.yaml`) is the input for client generation.
226247
* Includes **exception handling via `CustomerControllerAdvice`**.
227248
* Provides **unit tests** for both controller and service layers.
228-
* Profiles: `local` (default) and `dev` available — can be extended per environment.
229249
* Focused on clarity and minimal setup.
230250
* Optional: You can attach extra annotations (e.g., Jackson) to generated wrapper classes by setting
231251
`app.openapi.wrapper.class-extra-annotation` in `application.yml`.

docs/assets/css/custom.css

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@
1414
margin: 1.25rem 0;
1515
border-radius: 8px;
1616
}
17-
.callout.learn-more { border-left-color: #2563eb; }
1817

19-
.callout ul { margin: 0.25rem 0 0 1rem; }
20-
.callout li { margin: 0.25rem 0; }
18+
.callout.learn-more {
19+
border-left-color: #2563eb;
20+
}
21+
22+
.callout ul {
23+
margin: 0.25rem 0 0 1rem;
24+
}
25+
26+
.callout li {
27+
margin: 0.25rem 0;
28+
}

0 commit comments

Comments
 (0)