{"id":44642487,"url":"https://github.com/bsayli/spring-boot-openapi-generics-clients","last_synced_at":"2026-02-14T19:01:13.191Z","repository":{"id":311843697,"uuid":"1045192298","full_name":"bsayli/spring-boot-openapi-generics-clients","owner":"bsayli","description":"End-to-end generics-aware OpenAPI clients with a single canonical {data, meta} contract and RFC 9457 Problem Details — built on Spring Boot 3.5 and Java 21.","archived":false,"fork":false,"pushed_at":"2026-01-20T14:42:41.000Z","size":11552,"stargazers_count":12,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-20T19:37:33.252Z","etag":null,"topics":["api-client","code-generation","generics","httpclient5","java","mustache-templates","openapi","openapi-3","openapi-3-1","openapi-generator","problem-details","problem-details-for-http-apis","restclient","spring-boot-3","springdoc","swagger"],"latest_commit_sha":null,"homepage":"https://bsayli.github.io/spring-boot-openapi-generics-clients/","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bsayli.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-26T19:40:05.000Z","updated_at":"2026-01-20T14:44:36.000Z","dependencies_parsed_at":"2025-08-27T08:56:02.001Z","dependency_job_id":"d19a580c-8c69-4115-a31b-87f08666d8f6","html_url":"https://github.com/bsayli/spring-boot-openapi-generics-clients","commit_stats":null,"previous_names":["bsayli/spring-boot-openapi-generics-clients"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/bsayli/spring-boot-openapi-generics-clients","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bsayli%2Fspring-boot-openapi-generics-clients","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bsayli%2Fspring-boot-openapi-generics-clients/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bsayli%2Fspring-boot-openapi-generics-clients/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bsayli%2Fspring-boot-openapi-generics-clients/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bsayli","download_url":"https://codeload.github.com/bsayli/spring-boot-openapi-generics-clients/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bsayli%2Fspring-boot-openapi-generics-clients/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29452581,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T15:52:44.973Z","status":"ssl_error","status_checked_at":"2026-02-14T15:52:11.208Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-client","code-generation","generics","httpclient5","java","mustache-templates","openapi","openapi-3","openapi-3-1","openapi-generator","problem-details","problem-details-for-http-apis","restclient","spring-boot-3","springdoc","swagger"],"created_at":"2026-02-14T19:00:31.983Z","updated_at":"2026-02-14T19:01:13.122Z","avatar_url":"https://github.com/bsayli.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spring Boot OpenAPI Generics — Contract‑Driven, End‑to‑End Type Safety\n\n[![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)\n[![Release](https://img.shields.io/github/v/release/bsayli/spring-boot-openapi-generics-clients?logo=github\\\u0026label=release)](https://github.com/bsayli/spring-boot-openapi-generics-clients/releases/latest)\n[![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)\n[![Java](https://img.shields.io/badge/Java-21-red?logo=openjdk)](https://openjdk.org/projects/jdk/21/)\n[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5.10-green?logo=springboot)](https://spring.io/projects/spring-boot)\n[![OpenAPI Generator](https://img.shields.io/badge/OpenAPI%20Generator-7.19.0-blue?logo=openapiinitiative)](https://openapi-generator.tech/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/images/cover/cover.png\" alt=\"OpenAPI Generics Reference Setup\" width=\"720\"/\u003e\n  \u003cbr/\u003e\n  \u003cem\u003e\n    Generics-aware OpenAPI client generation using a single shared response contract —\n    no duplicated envelopes, no client-side drift.\n  \u003c/em\u003e\n\u003c/p\u003e\n\nThis repository is a **reference implementation** demonstrating\n**generics-aware OpenAPI client generation** with **Spring Boot**, **Springdoc**, and **OpenAPI Generator**.\n\nIt demonstrates a **single-contract approach** where **server and client share\nthe same canonical response model**:\n\n```java\nServiceResponse\u003cT\u003e\n```\n\n- No duplicated envelopes\n- No parallel client contracts\n- No generics erased at generation time\n\nThe result is a **deterministic, type‑safe API boundary** with **Page‑aware generics** and **RFC 9457‑compliant error handling**.\n\n---\n\n## 📑 Table of Contents\n\n* 📦 [Modules](#-modules)\n* ⚡ [Quick Start](#-quick-start)\n* 🚨 [The Problem](#-the-problem)\n* 💡 [The Core Idea](#-the-core-idea)\n* 🧱 [Canonical Contract](#-canonical-contract)\n* 🏗 [Architecture Overview](#-architecture-overview)\n* 🔎 [Proof: Generated Client Models (Before/After)](#-proof-generated-client-models-beforeafter)\n* 🧩 [Example Responses](#-example-responses)\n* 🧠 [Design Guarantees](#-design-guarantees)\n* 📘 [Adoption Guides](#-adoption-guides)\n* 🔗 [References \u0026 External Links](#-references--external-links)\n\n---\n\n## 📦 Modules\n\n* **[api-contract](api-contract/README.md)**  \n  Shared, framework-agnostic API contract defining the canonical `{ data, meta }` response model,  \n  pagination primitives, and RFC 9457 error extensions.  \n  This module is the **single source of truth** shared by both server and client.\n\n* **[customer-service](customer-service/README.md)**  \n  Spring Boot API producer exposing a deterministic **OpenAPI 3.1** specification enriched with  \n  generics semantics (`ServiceResponse\u003cT\u003e`, `ServiceResponse\u003cPage\u003cT\u003e\u003e`).\n\n* **[customer-service-client](customer-service-client/README.md)**  \n  Generated Java client that **reuses the canonical contract** and preserves generics  \n  without duplicating envelopes or paging models.\n\n---\n\n## ⚡ Quick Start\n\nThis repository uses an **aggregator (root) build** to guarantee that the shared **`api-contract`** module is always available to both the server and the client.\nFor first-time users, **start from the repo root**.\n\n---\n\n### ✅ Option A — Recommended (Deterministic, First-Time Setup)\n\nThis is the **canonical way** to get everything running after cloning the repository.\nIt installs `api-contract` locally and builds all modules in the correct order.\n\n```bash\n# 1) Build everything once from the repo root\nmvn -q -ntp clean install\n\n# 2) Run the backend service\ncd customer-service \u0026\u0026 mvn -q -ntp spring-boot:run\n```\n\nAt this point:\n\n* `api-contract` is installed into your local Maven repository\n* `customer-service` is running\n* `customer-service-client` has been generated and compiled\n\nNo additional setup is required.\n\n---\n\n### 🔄 Option B — Regenerate the Client from the Live OpenAPI Spec\n\nUse this flow **only when you change the server contract** and want to regenerate\nclient wrappers from the live OpenAPI definition.\n\n```bash\n# 1) Ensure the backend is running\ncd customer-service \u0026\u0026 mvn -q -ntp spring-boot:run\n\n# 2) Pull the OpenAPI spec into the client module\ncd ../customer-service-client\ncurl -s http://localhost:8084/customer-service/v3/api-docs.yaml \\\n  -o src/main/resources/customer-api-docs.yaml\n\n# 3) Regenerate and build the client\nmvn -q -ntp clean install\n```\n\nThis regenerates **thin wrappers** extending the canonical contract:\n\n```java\nServiceResponse\u003cT\u003e\nServiceResponse\u003cPage\u003cT\u003e\u003e\n```\n\n---\n\n### 📂 Generated Sources\n\nGenerated client sources are written to:\n\n```\ncustomer-service-client/target/generated-sources/openapi/src/gen/java\n```\n\nThey are **automatically added to compilation** via `build-helper-maven-plugin`.\n\n---\n\n### 📝 Notes\n\n* You do **not** need to manually build or install `api-contract`.\n  The root build handles this by design.\n* If you skip the root build and run the client directly, the build may fail\n  because `api-contract` is not yet available.\n* For CI and local parity, all commands use `-ntp` (no transfer progress).\n\n---\n\n\u003e **Rule of thumb:**\n\u003e - If you just cloned the repo → **build from root**\n\u003e - If you changed the API contract → **regenerate the client**\n\n---\n\n## 🚨 The Problem\n\nMost real‑world APIs wrap responses with:\n\n* metadata (pagination, sorting, timestamps)\n* payload data\n* standardized error objects\n\nYet OpenAPI‑based generators typically:\n\n* erase generics\n* duplicate response envelopes per endpoint\n* break type safety for nested containers\n\nResulting in clients like:\n\n```java\n// Typical generated output (problematic)\nclass ServiceResponseCustomerDto {\n  CustomerDto data;\n  Meta meta;\n}\n\nclass ServiceResponsePageCustomerDto {\n  PageCustomerDto data; // lost Page\u003cCustomerDto\u003e\n  Meta meta;\n}\n```\n\nThis scales poorly and makes contract evolution painful.\n\n---\n\n\u003e **Background:** The rationale behind the canonical `ServiceResponse\u003cT\u003e` contract is explained here (updated Jan 2026):  \n\u003e [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)\n---\n\n## 💡 The Core Idea\n\n**Treat the response envelope as a shared contract — not a generated artifact.**\n\n* The server **publishes intent**, not Java shapes.\n* The client **reuses the same contract types**.\n* OpenAPI is used as a **semantic bridge**, not a code generator of truth.\n\nEverything revolves around a single, stable abstraction:\n\n```java\nServiceResponse\u003cT\u003e\n```\n\n---\n\n## 🧱 Canonical Contract\n\nAll successful responses — on **both server and client** — use:\n\n```java\nServiceResponse\u003cT\u003e\n```\n\nProvided by the shared module:\n\n```\nio.github.bsayli:api-contract\n```\n\n### Supported Shapes\n\n| Shape                      | Supported | Notes                                               |\n| -------------------------- | --------- | --------------------------------------------------- |\n| `ServiceResponse\u003cT\u003e`       | ✅        | Canonical success envelope (guaranteed)             |\n| `ServiceResponse\u003cPage\u003cT\u003e\u003e` | ✅        | **Guaranteed nested generic**                       |\n| `ServiceResponse\u003cList\u003cT\u003e\u003e` | ⚠️        | Uses OpenAPI Generator default behavior (unchanged) |\n| Arbitrary nested generics  | ❌        | Outside the supported contract                      |\n\nThis implementation **does not alter or restrict** OpenAPI Generator’s default handling\nof standard collection types such as `List\u003cT\u003e`.\n\nIt **defines explicit guarantees only** for:\n- `ServiceResponse\u003cT\u003e`\n- `ServiceResponse\u003cPage\u003cT\u003e\u003e`\n\nAll other shapes follow the generator’s default behavior and are intentionally kept\n**outside the canonical contract** to preserve deterministic schemas and\ngenerator-safe evolution.\n\n---\n\n## 🏗 Architecture Overview\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/images/architecture/architectural-diagram.png\"\n       alt=\"Generics-Aware OpenAPI Contract Flow\"\n       width=\"900\"/\u003e\n  \u003cbr/\u003e\n  \u003cem\u003e\n    Contract-driven, generics-aware OpenAPI setup —\n    from Spring Boot producer to type-safe client generation and consumption.\n  \u003c/em\u003e\n\u003c/p\u003e\n\n```\n[service-api]\n   └─ publishes OpenAPI 3.1 specification\n        └─ enriched with wrapper semantics (vendor extensions)\n              │\n              ▼\n[generated client]\n   └─ thin wrapper models extending ServiceResponse\u003cT\u003e\n        └─ APIs + RestClient + ApiClient (invoker layer)\n              │\n              ▼\n[consumer application]\n   └─ depends only on adapter interfaces\n```\n\n### Design Choices\n\n* **Contract-first** — the OpenAPI specification describes *API contracts*, not implementations.\n* **Single canonical envelope** — all successful responses share a unified `{ data, meta }` shape via `ServiceResponse\u003cT\u003e`.\n* **Explicit generic scope** — nested generics are intentionally limited to `ServiceResponse\u003cPage\u003cT\u003e\u003e`.\n* **Generator-safe modeling** — thin wrapper classes are emitted via Mustache overlays, not handwritten code.\n* **Adapter boundary** — consumer applications depend on stable adapters, not on generated APIs directly.\n\n### Layers at a glance\n\n| Layer                       | Responsibility                                                                                                             |\n| --------------------------- |----------------------------------------------------------------------------------------------------------------------------|\n| **API Producer (Server)**   | Spring Boot service publishing an **OpenAPI 3.1** spec via Springdoc, backed by the shared `api-contract`                  |\n| **OpenAPI Schema Enricher** | `OpenApiCustomizer` detecting `ServiceResponse\u003cT\u003e` and emitting vendor extensions (`x-api-wrapper`, `x-data-container`, …) |\n| **Code Generation**         | OpenAPI Generator **7.19.0** (`java / restclient`) with Mustache overlays bound to the canonical contract                  |\n| **Generated Client**        | Thin wrapper models, domain DTOs, APIs, `RestClient`, and `ApiClient` (invoker layer)                                      |\n| **Error Handling**          | RFC 9457 **Problem Details** decoded into `ApiProblemException` with extension support                                     |\n| **API Consumer**            | Application/service layer using adapter interfaces and receiving fully type-safe responses                                 |\n\n\u003e **Key principle**\n\u003e\n\u003e The OpenAPI specification is the *single source of truth*.  \n\u003e Generated code is disposable; contracts are not.\n\n---\n\n## 🔎 Proof: Generated Client Models (Before/After)\n\n**Before (duplicated models):**\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/images/proof/generated-client-wrapper-before.png\" width=\"800\"/\u003e\n\u003c/p\u003e\n\n**After (thin wrappers):**\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/images/proof/generated-client-wrapper-after.png\" width=\"800\"/\u003e\n\u003c/p\u003e\n\n```java\npublic class ServiceResponsePageCustomerDto\n    extends ServiceResponse\u003cPage\u003cCustomerDto\u003e\u003e {}\n```\n\nNo duplicated envelope. No lost generics.\n\n---\n\n## 🧩 Example Responses\n\n### Single Item\n\n```json\n{\n  \"data\": { \"customerId\": 1, \"name\": \"Jane Doe\" },\n  \"meta\": { \"serverTime\": \"2025-01-01T12:34:56Z\", \"sort\": [] }\n}\n```\n\n### Paged\n\n```json\n{\n  \"data\": {\n    \"content\": [ { \"customerId\": 1 }, { \"customerId\": 2 } ],\n    \"page\": 0,\n    \"size\": 5,\n    \"totalElements\": 37,\n    \"totalPages\": 8,\n    \"hasNext\": true,\n    \"hasPrev\": false\n  },\n  \"meta\": { \"serverTime\": \"2025-01-01T12:34:56Z\" }\n}\n```\n\n---\n\n## 🧠 Design Guarantees\n\nThis repository guarantees:\n\n* **One shared response contract** across server and client\n* **No duplicated envelopes** in generated clients\n* **Page-only nested generics** (`ServiceResponse\u003cPage\u003cT\u003e\u003e`)\n* **Deterministic schema names** for the guaranteed shapes\n* **RFC 9457-first error handling** (Problem Details)\n* **Generator-safe evolution** through minimal, explicit template overlays\n\nThis is not a demo.  \nIt is a **reference implementation**.\n\n---\n\n## 📘 Adoption Guides\n\nStep-by-step integration guides live under [`docs/adoption`](docs/adoption):\n\n- **[Server-Side Adoption](docs/adoption/server-side-adoption.md)** — Publish a deterministic, generics-aware OpenAPI 3.1 contract.\n- **[Client-Side Adoption](docs/adoption/client-side-adoption.md)** — Configure Maven, OpenAPI Generator, and Mustache templates (build-time setup only).\n---\n\n## 🔗 References \u0026 External Links\n\n- 📘 **Adoption Guide (GitHub Pages)**  \n  [Spring Boot OpenAPI Generics — Adoption Guide](https://bsayli.github.io/spring-boot-openapi-generics-clients/)\n\n- ✍️ **Medium Article**  \n  [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)\n\n- 📄 **RFC 9457**  \n  [Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457)\n\n---\n\n\n## 🤝 Contributing \u0026 Feedback\n\nThis repository is a **reference implementation**, not a closed framework.\n\nIf you:\n\n* apply this pattern in a real project,\n* spot an inconsistency,\n* or want to evolve the shared contract or generator templates,\n\nfeel free to open an issue or start a discussion.\n\n👉 [Discussions](https://github.com/bsayli/spring-boot-openapi-generics-clients/discussions)\n\nEven short, practical feedback helps refine the pattern.\n\n---\n\n## 🛡 License\n\nLicensed under **MIT** — see [LICENSE](LICENSE).\n\nAll modules inherit the same license.\n\n---\n\n**Barış Saylı**\n[GitHub](https://github.com/bsayli) · [Medium](https://medium.com/@baris.sayli) · [LinkedIn](https://www.linkedin.com/in/bsayli)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbsayli%2Fspring-boot-openapi-generics-clients","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbsayli%2Fspring-boot-openapi-generics-clients","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbsayli%2Fspring-boot-openapi-generics-clients/lists"}