Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b2cdfba
detect generics
Picazsoo Apr 23, 2026
88a7ec5
detect generics v2
Picazsoo Apr 23, 2026
cc3172c
multi generics
Picazsoo Apr 23, 2026
8cc4a46
multi generics - kotlin
Picazsoo Apr 23, 2026
f279a84
add javadoc
Picazsoo Apr 23, 2026
cea9861
Enhance pageable validation by adding minSize and minPage constraints…
Picazsoo May 5, 2026
2b24608
update samples
Picazsoo May 5, 2026
1e4013a
fix test
Picazsoo May 5, 2026
e133715
Merge branch 'master' into bugfix/pageable-scan-resolve-allof-constra…
Picazsoo May 5, 2026
0701d5b
use ModelUtils in SpringPageableScanUtils
Picazsoo May 5, 2026
b463d88
update tests in samples
Picazsoo May 6, 2026
cec4212
move reusable code to shared ModelUtils.java
Picazsoo May 6, 2026
7151bb3
add tests
Picazsoo May 6, 2026
daa33e0
fix CR suggestion and add tests
Picazsoo May 6, 2026
609fc87
suppress paged schemas correctly when modelNameSuffix/Prefix is set
Picazsoo May 6, 2026
ffdb9e3
refactor: enhance pageable constraints handling with exclusive bounds…
Picazsoo May 7, 2026
86992bf
DRY code
Picazsoo May 7, 2026
25f1f8f
refactor: improve resolution of nested allOf constraints for maximum …
Picazsoo May 7, 2026
4c7032c
re-enable test
Picazsoo May 7, 2026
ea9937a
test: fix implementation and add unit test for resolving default in n…
Picazsoo May 7, 2026
4c76d10
add comment to explain that behavior around default is undefined in o…
Picazsoo May 7, 2026
edd4a5f
Merge branch 'master' into bugfix/pageable-scan-resolve-allof-constra…
Picazsoo May 14, 2026
5e85d53
pageable-scan-resolve-allof-constraints refactor: streamline pageable…
Picazsoo May 14, 2026
5eba333
feat: add support for ValuedEnum interface in code generation for enums
Picazsoo May 14, 2026
772ff0e
fix: remove Pageable support for non-spring-boot libraries and update…
Picazsoo May 15, 2026
011c7b7
update documentation
Picazsoo May 15, 2026
90282ea
fix: update default resolution logic for allOf schemas to last-writer…
Picazsoo May 15, 2026
94ed6c5
fix: ensure correct parent reference ordering in allOf for default re…
Picazsoo May 15, 2026
e2d5b2b
Merge branch 'master' into bugfix/pageable-scan-resolve-allof-constra…
Picazsoo May 16, 2026
78d8574
Merge branch 'master' into bugfix/pageable-scan-resolve-allof-constra…
Picazsoo May 18, 2026
07f6388
DRY code
Picazsoo May 18, 2026
5bc80c6
DRY code
Picazsoo May 18, 2026
a00b0a7
fix after merge of master
Picazsoo May 20, 2026
3a6c41c
revert default-resolution related changes as the behavior is unspecif…
Picazsoo May 20, 2026
1f6e6ed
add missing PetApiController implementations and validation tests for…
Picazsoo May 20, 2026
89db019
Merge branch 'master' into bugfix/pageable-scan-resolve-allof-constra…
Picazsoo May 24, 2026
ebdfe33
Merge remote-tracking branch 'origin/master' into feature/detect-gene…
Picazsoo May 30, 2026
4f7a6dc
Merge branch 'master' into bugfix/pageable-scan-resolve-allof-constra…
Picazsoo May 30, 2026
0fdea50
Merge branch 'bugfix/pageable-scan-resolve-allof-constraints' into fe…
Picazsoo May 30, 2026
a958ee6
fix(spring): re-key GenericSubstitutionSupport instanceRegistry after…
Picazsoo May 30, 2026
3ca0de0
fix: property-level generic substitution + recursive type-arg expansion
Picazsoo May 30, 2026
3b90176
feat: respect schemaMapping in genericPatterns + warn on Mode B impor…
Picazsoo May 30, 2026
6d20f38
feat: unify substituteGenericPagedModel under GenericSubstitutionSupport
Picazsoo May 30, 2026
f316dd5
refactor: post-review fixes for genericPatterns + paged-model unifica…
Picazsoo May 31, 2026
de99a89
docs: document genericPatterns and discoverGenericPatterns options
Picazsoo May 31, 2026
3f76733
feat(spring): discoverGenericPatterns now suggests slotArray patterns…
Picazsoo May 31, 2026
7ae7924
feat(spring): discoverGenericPatterns now supports allOf schemas
Picazsoo May 31, 2026
0ae4e39
fix(kotlin-spring,core): address 2 review findings on this branch
Picazsoo May 31, 2026
50e723a
update samples
Picazsoo May 31, 2026
32e9ca6
Fix ArchUnit logger violations on three classes
Picazsoo May 31, 2026
54f2b70
Fix three regressions surfaced by the failing test suite
Picazsoo May 31, 2026
7c9a072
update samples
Picazsoo May 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions bin/configs/kotlin-spring-boot-generics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
generatorName: kotlin-spring
outputDir: samples/server/petstore/kotlin-springboot-generics
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-generics.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
additionalProperties:
documentationProvider: none
annotationLibrary: none
useSwaggerUI: "false"
serializableModel: "true"
useBeanValidation: "true"
interfaceOnly: "true"
skipDefaultInterface: "true"
useSpringBoot3: "true"
hideGenerationTimestamp: "true"
useTags: "true"
requestMappingMode: api_interface
genericPatterns:
- suffix: Response
genericClass: ApiResponse
slot: data
- suffix: Page
genericClass: org.springframework.data.domain.Page
slotArray: content
- suffix: ErrorResult
genericClass: Result
slots:
data: T
error: E
discoverGenericPatterns: "true"
30 changes: 30 additions & 0 deletions bin/configs/spring-boot-generics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
generatorName: spring
outputDir: samples/server/petstore/springboot-generics
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-generics.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
additionalProperties:
documentationProvider: none
annotationLibrary: none
useSwaggerUI: "false"
serializableModel: "true"
useBeanValidation: "true"
interfaceOnly: "true"
skipDefaultInterface: "true"
useSpringBoot3: "true"
hideGenerationTimestamp: "true"
useTags: "true"
requestMappingMode: api_interface
genericPatterns:
- suffix: Response
genericClass: ApiResponse
slot: data
- suffix: Page
genericClass: org.springframework.data.domain.Page
slotArray: content
- suffix: ErrorResult
genericClass: Result
slots:
data: T
error: E
discoverGenericPatterns: "true"
176 changes: 176 additions & 0 deletions docs/customization-genericPatterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
id: customization-generic-patterns
title: Generic Schema Substitution (Spring / Kotlin-Spring)
---

# Generic Schema Substitution (`genericPatterns`)

> Applies to the `spring` and `kotlin-spring` generators.

OpenAPI 3 has no native way to express *parametric* schemas — schemas that share a common shape but differ in one type. In practice many specs end up duplicating wrapper schemas:

```yaml
UserResponse:
type: object
properties:
data: { $ref: '#/components/schemas/User' }
requestId: { type: string }
OrderResponse:
type: object
properties:
data: { $ref: '#/components/schemas/Order' }
requestId: { type: string }
PetResponse:
type: object
properties:
data: { $ref: '#/components/schemas/Pet' }
requestId: { type: string }
```

Without this feature, the generator creates three nearly-identical `*Response` classes. With `genericPatterns`, the generator detects the family and replaces all references with a single `ApiResponse<T>` (either imported from your codebase or generated for you), and the redundant wrapper schemas are removed from the generated model package.

## Quick start

```yaml
# openapitools-generator-maven-plugin config (or YAML config file)
additionalProperties:
genericPatterns:
- suffix: Response
genericClass: com.example.ApiResponse
slot: data
```

After generation, every operation that previously returned `UserResponse` now returns `ApiResponse<User>`, properties of type `UserResponse` are rewritten too, and the three `*Response` model classes are no longer generated.

> ⚠️ Substitution and suppression of wrapper schemas only happen when `annotationLibrary=none` (Swagger / OpenAPI annotations on generated models reference the concrete classes, so they must be kept). Return-type substitution itself happens regardless.

## Pattern matching

Each entry in `genericPatterns` matches schemas by *name*:

| Field | Purpose | Required? |
|---|---|---|
| `suffix` | Schema name ends with this string (e.g. `Response` matches `UserResponse`) | exactly one of `suffix` or `prefix` |
| `prefix` | Schema name starts with this string (e.g. `Api` matches `ApiUser`) | exactly one of `suffix` or `prefix` |
| `genericClass` | Target generic class. Mode A (FQN) imports an external class; Mode B (simple name) generates a class in `configPackage`. | yes |
| `slot` | Property name whose `$ref` becomes `T`. Single type parameter. | one of `slot` / `slotArray` / `slots` |
| `slotArray` | Array property name whose `items.$ref` becomes `T`. Single type parameter. | one of `slot` / `slotArray` / `slots` |
| `slots` | Map of `propertyName: typeParamName` for multi-parameter generics (e.g. `{data: T, error: E}`). | one of `slot` / `slotArray` / `slots` |

## Mode A vs Mode B

The form of `genericClass` decides whether a class file is generated:

* **Mode A** — `genericClass` contains a dot (`.`). Treated as a fully-qualified class name; only an `importMapping` entry is added. **Use this when the class already exists** in your codebase or in a library:

```yaml
- suffix: Response
genericClass: com.acme.api.ApiResponse # already exists in com.acme.api
slot: data
```

* **Mode B** — `genericClass` is a simple name (no dot). A new source file is generated in `configPackage` (defaults to `org.openapitools.configuration`). The generated class mirrors the non-slot properties of the matched schemas and declares the configured type parameters:

```yaml
- suffix: Page
genericClass: ApiPage # creates ApiPage.java/kt with the common props + <T>
slotArray: content
```

## Multi-slot generics

Use `slots` (instead of `slot` / `slotArray`) to map multiple properties to multiple type parameters:

```yaml
genericPatterns:
- suffix: ErrorResult
genericClass: Result # generated class Result<T, E>
slots:
data: T # 'data' property → T
error: E # 'error' property → E
```

A spec schema `UserValidationErrorResult` (with `data: $ref User`, `error: $ref ValidationError`) becomes `Result<User, ValidationError>` everywhere it appears.

Array-ness of each slot property is auto-detected from the matched schema — you do not need to declare it.

## Vendor-extension overrides

Patterns are a heuristic. If a single schema needs to be substituted differently (or excluded), declare the substitution inline:

```yaml
components:
schemas:
SearchPage:
x-generic:
class: org.springframework.data.domain.Slice
slot: content
type: object
properties:
content:
type: array
items: { $ref: '#/components/schemas/SearchResult' }
hasNext: { type: boolean }
```

Vendor-extension declarations take precedence over both name-pattern matches and structurally-detected paged models.

## Discovery (`discoverGenericPatterns`)

If you don't yet know which schemas in your spec are good candidates, enable:

```yaml
additionalProperties:
discoverGenericPatterns: true
```

During the next generation, the tool scans for **structural clusters** — groups of 2+ schemas with identical property structure except for one varying `$ref` property — and logs a ready-to-paste `genericPatterns:` YAML block at **INFO** level. No substitution is applied; it is purely a suggestion.

> ℹ️ To see the suggestions, ensure your logging configuration emits INFO-level messages for `org.openapitools.codegen.languages.GenericSchemaScanUtils`. The Maven plugin shows them by default; CLI users may need `--verbose`.

### What discovery does *not* find

Discovery is intentionally limited to **single-slot** patterns. The slot itself may be either a plain `$ref` (suggested as `slot:`) or an `array` of `$ref` (suggested as `slotArray:`, which covers the typical `Page<T>` shape: `{ content: array of $ref, page: $ref Metadata }`). Both flat-object and `allOf`-based schemas are scanned; for `allOf` shapes the extends-bases are part of the structural fingerprint, so members that extend different bases (e.g. `UserPage extends PageMeta` vs. `OrderPage extends CursorMeta`) will *not* be incorrectly clustered together. Discovery will **not** suggest:

* **Multi-slot generics** (e.g. `Result<T, E>`) — only single-slot families are auto-detected.
* **`allOf` schemas with more than one inline-object entry** — ambiguous which entry owns the slot, so they are skipped.
* **Schemas that don't share a common name suffix** — clustering also needs a stable naming convention to suggest a usable pattern.

> **Note**: paged-`allOf` clusters (e.g. `UserPage` / `OrderPage` extending a shared `PageMeta`) *will* now show up as `slotArray:` suggestions. For pure pagination cases prefer [`substituteGenericPagedModel`](#companion-feature-substitutegenericpagedmodel) — it's auto-applied (no pattern config), structurally detects both flat and `allOf` paged shapes, and removes the orphaned metadata schemas in one go.

For any of these, fall back to a hand-written `genericPatterns` entry or a `x-generic` vendor extension on the individual schema.

## Companion feature: `substituteGenericPagedModel`

A purely structural variant exists for the very common Spring `PagedModel<T>` case:

```yaml
additionalProperties:
substituteGenericPagedModel: true
```

This requires *no* pattern config and *no* naming convention — the generator detects any schema with a `content` array property and a pagination-metadata `$ref` (e.g. a `PageMetadata`-style sibling), in both flat-object and `allOf` forms. The detected paged schemas are replaced with `PagedModel<T>` and the orphaned metadata schemas are suppressed.

Internally this routes through the same substitution engine as `genericPatterns`, so all the interactions described below apply.

## Interaction with other options

| Option | Effect |
|---|---|
| `schemaMapping` | A schema name present in `schemaMapping` is **never** substituted by `genericPatterns` (the user-declared mapping wins). The corresponding companion meta-schema is kept alive too if any sibling main is still mapped. |
| `importMapping` | Mode A registers its FQN here automatically. For `substituteGenericPagedModel`, set `importMapping.PagedModel` (or any custom name) to override the default Spring class. |
| `modelNameSuffix` / `modelNamePrefix` / `modelNameMapping` | Fully supported. Registry keys are re-keyed via `toModelName()` so lookups by the transformed name work. Two raw names collapsing to the same transformed name will emit a `WARN` log and only one substitution will apply. |
| `annotationLibrary != none` | Return-type substitution still runs, but wrapper / meta schemas are kept (annotations like `@ApiResponse`, `@Schema` reference them by class). |

## Examples in the repository

* `bin/configs/spring-boot-generics.yaml` and `bin/configs/kotlin-spring-boot-generics.yaml` — runnable end-to-end configurations covering all three pattern forms plus discovery.
* `samples/server/petstore/springboot-generics/` — generated output from the above config.
* The input spec at `modules/openapi-generator/src/test/resources/3_0/spring/petstore-generics.yaml` exercises single-slot, multi-slot, array-slot, vendor-extension, and paged-model variants together.

## Limitations and notes

* The features are currently implemented only for the `spring` and `kotlin-spring` generators.
* Pattern matching is purely name-based — schemas that do not share a naming convention are not detected by tier-2 patterns (use vendor extensions on those schemas, or rely on `discoverGenericPatterns` / `substituteGenericPagedModel`).
* Suppression of substituted schemas is gated on `annotationLibrary=none` (see above).
* The data class powering this is documented in detail in [`GenericPatternConfig.java`](https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GenericPatternConfig.java).
5 changes: 4 additions & 1 deletion docs/generators/java-camel.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|disableDiscriminatorJsonIgnoreProperties|Ignore discriminator field type for Jackson serialization| |false|
|disableHtmlEscaping|Disable HTML escaping of JSON strings when using gson (needed to avoid problems with byte[] fields)| |false|
|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|<dl><dt>**false**</dt><dd>The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.</dd><dt>**true**</dt><dd>Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.</dd></dl>|true|
|discoverGenericPatterns|When true, scans schemas for structural clusters (groups of schemas with the same structure except for one varying $ref property) and logs them as INFO-level suggestions for configuring genericPatterns. Never auto-applies substitution.| |false|
|discriminatorCaseSensitive|Whether the discriminator value lookup should be case-sensitive or not. This option only works for Java API client| |true|
|documentationProvider|Select the OpenAPI documentation provider.|<dl><dt>**none**</dt><dd>Do not publish an OpenAPI specification.</dd><dt>**source**</dt><dd>Publish the original input OpenAPI specification.</dd><dt>**springdoc**</dt><dd>Generate an OpenAPI 3 specification using SpringDoc.</dd></dl>|springdoc|
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
Expand All @@ -67,6 +68,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|generatePageableConstraintValidation|Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.| |false|
|generateSortValidation|Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.| |false|
|generatedConstructorWithRequiredArgs|Whether to generate constructors with required args for models| |true|
|genericPatterns|List of generic substitution patterns. Each entry specifies a suffix or prefix to match schema names against, a target generic class (FQN for import-only Mode A, or simple name for generated Mode B), and the slot or slotArray property that becomes the type parameter T. Example (YAML config): genericPatterns: [{suffix: Response, genericClass: ApiResponse, slot: data}]. See GenericPatternConfig for full documentation.| |null|
|groupId|groupId in generated pom.xml| |org.openapitools|
|hateoas|Use Spring HATEOAS library to allow adding HATEOAS links| |false|
|hideGenerationTimestamp|Hides the generation timestamp when files are generated.| |false|
Expand Down Expand Up @@ -112,6 +114,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useBeanValidation|Use BeanValidation API annotations| |true|
|useDeductionForOneOfInterfaces|Annotate discriminator-free oneOf interfaces with Jackson's @JsonTypeInfo(use = Id.DEDUCTION) and @JsonSubTypes so the concrete subtype is resolved from the JSON field set rather than a type-tag property. Has no effect when a discriminator is present (name-based resolution is used instead). Requires subtypes to have structurally distinct sets of properties.| |false|
|useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
|useEnumValueInterface|Generate a ValuedEnum&lt;T&gt; interface in the config package and make all generated enums implement it, providing a common typed way to access the underlying enum value. Use `importMappings.ValuedEnum` to substitute a custom/library-provided interface instead of generating one.| |false|
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
|useHttpServiceProxyFactoryInterfacesConfigurator|Generate HttpInterfacesAbstractConfigurator based on an HttpServiceProxyFactory instance (as opposed to a WebClient instance, when disabled) for generating Spring HTTP interfaces.| |false|
Expand Down Expand Up @@ -145,7 +148,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|x-class-extra-annotation|List of custom annotations to be added to model|MODEL|null
|x-field-extra-annotation|List of custom annotations to be added to property|FIELD, OPERATION_PARAMETER|null
|x-operation-extra-annotation|List of custom annotations to be added to operation|OPERATION|null
|x-spring-paginated|Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object.|OPERATION|false
|x-spring-paginated|Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object. Only applies when `library=spring-boot`; ignored for client libraries (spring-cloud, spring-declarative-http-interface).|OPERATION|false
|x-version-param|Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false|OPERATION_PARAMETER|null
|x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null
|x-size-message|Add this property whenever you need to customize the invalidation error message for the size or length of a variable|FIELD, OPERATION_PARAMETER|null
Expand Down
Loading
Loading