Skip to content

Feature/detect generics allof fix#23917

Draft
Picazsoo wants to merge 52 commits into
OpenAPITools:masterfrom
Picazsoo:feature/detect-generics-allof-fix
Draft

Feature/detect generics allof fix#23917
Picazsoo wants to merge 52 commits into
OpenAPITools:masterfrom
Picazsoo:feature/detect-generics-allof-fix

Conversation

@Picazsoo
Copy link
Copy Markdown
Contributor

@Picazsoo Picazsoo commented May 31, 2026

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Add generic schema substitution to spring and kotlin-spring to collapse repeated wrapper schemas into real generics (e.g., ApiResponse), unify paged-model handling under the same system, and improve pageable validation and enum ergonomics. Adds discovery to suggest genericPatterns and updates docs and samples.

  • New Features

    • genericPatterns: configure suffix/prefix patterns with slot/slotArray to substitute return and property types; safely suppress redundant schemas; works with schemaMapping, importMapping, and modelNamePrefix/modelNameSuffix.
    • discoverGenericPatterns: scan specs and log ready-to-copy suggestions (supports arrays like Page<T> and allOf-based shapes).
    • Unified paged models: paginated schemas are auto-detected and substituted (e.g., UserPagePage<User>), with safe removal of orphaned meta-schemas.
    • useEnumValueInterface: generate a ValuedEnum<T> in configPackage and have all enums implement it; override via importMappings.ValuedEnum.
    • Codegen hook: prepareSupportingFile(bundle, file) to adjust per-file data. New docs at docs/customization-genericPatterns.md; sample configs and projects added for both generators.
  • Bug Fixes

    • Preserve outer containers during substitution (e.g., List<UserResponse>List<ApiResponse<User>>) and correctly handle array properties and imports.
    • Correct suppression and import syncing when modelNamePrefix/modelNameSuffix is set; honor explicit schemaMapping overrides.
    • Pageable: add minSize/minPage; resolve min/max and defaults through nested allOf; apply annotations consistently; limit Pageable features to Spring Boot libraries and update docs.
    • Safer schema suppression by checking references across operations, models, and imports.
    • Kotlin enums: avoid emitting override val value for container inline enums when useEnumValueInterface is on (fixes “overrides nothing”); bind useEnumValueInterface from additionalProperties so the feature actually generates ValuedEnum and implements it.
    • Generator: only call prepareSupportingFile for files that will render and reset per-file data to prevent leakage between supporting files (stabilizes Mode B genericClass generation).
    • Code health: fix ArchUnit logger rule violations by scoping loggers correctly.

Written for commit 7c9a072. Summary will update on new commits.

Review in cubic

Picazsoo and others added 30 commits April 24, 2026 00:15
…. Search also allOf references for constraints and defaults
… handling and enhance annotation application logic
…ints

Resolved 4 conflicts by keeping both sides' additions:

- KotlinSpringServerCodegen.java: added openApiNullable field alongside
  existing useEnumValueInterface/valuedEnumClassName fields; added
  OPENAPI_NULLABLE CLI option alongside USE_ENUM_VALUE_INTERFACE option.
  All other openApiNullable logic (import mappings, processOpts,
  postProcessModelProperty, postProcessModelsEnum) merged cleanly.

- KotlinSpringServerCodegenTest.java: kept new useEnumValueInterface tests
  (branch) and new required+nullable 4-state scenario tests (master).
  Removed the now-obsolete shouldRefuseOpenApiNullableWithJackson3 test
  (jackson-databind-nullable >= 0.2.10 supports both Jackson 2 and 3).

- petstore-sort-validation.yaml: kept branch pageable allOf constraint
  endpoints + kept master NullableModel endpoints and schema.

- springboot-sort-validation/openapi.yaml: same — kept both sets of
  generated endpoints and schemas.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picazsoo and others added 17 commits May 18, 2026 10:20
…ints

# Conflicts:
#	modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
…rics

# Conflicts:
#	modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java
#	modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
#	modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java
… toModelName()

Mirrors the modelNameSuffix/Prefix fix done for PagedModelScanUtils in
bugfix/pageable-scan-resolve-allof-constraints.

Before this fix, instanceRegistry was keyed by raw spec schema names
(e.g. 'UserResponse'), while op.returnBaseType and objs keys are already
transformed via toModelName() (e.g. 'UserResponseDto'). This caused:
  1. substituteReturnType: registry lookup missed -> no substitution
  2. suppressGenericSchemas: objs.remove used wrong (transformed) key
  3. substituteReturnType: import removal used raw name, not transformed

Fix:
- preprocessOpenAPI: re-key instanceRegistry with ctx.toModelName() after
  all three tiers have populated it
- substituteReturnType: use ctx.toModelName(inst.schemaName) for import
  removal, since op.imports holds transformed names
- suppressGenericSchemas: use inst.schemaName (raw, preserved in
  GenericInstance) for objs.remove(), not the now-transformed map key

Tests added for SpringCodegen and KotlinSpringServerCodegen verifying
that genericPatterns substitution and schema suppression work correctly
when modelNameSuffix or modelNamePrefix is configured.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Substitute generic-instance schema refs in model properties (not just
  operation return types) so that schemas like OrderDetails.userResult
  rendered as ApiResponse<User> when UserResponse is a detected instance.

- For array properties, use prop.complexType as fallback lookup key
  (prop.baseType = 'List' for arrays; item type is in complexType).
  Also update prop.items.dataType / datatypeWithEnum so that addXxxItem
  helper methods in pojo.mustache show the correct substituted type.

- Iterate all property lists with IdentityHashMap deduplication:
  CodegenModel.removeAllDuplicatedProperty() clones vars, requiredVars,
  and optionalVars into independent instances. Kotlin Spring templates
  render from requiredVars/optionalVars, not vars, so all lists must be
  updated.

- Add toModelImport(String) to Context interface (Spring + Kotlin impls)
  and sync ModelsMap.imports (pre-built List before postProcessAllModels)
  with removals and additions so import statements in generated files
  are correct even with modelNameSuffix/Prefix.

- Fix Scenario E suppression-safety tests: allOf without discriminator
  uses composition (model.parent = null), so the generic instance CAN be
  safely suppressed. Tests updated to assert UserResponse is absent.

- Fix Kotlin Scenario C test: kotlinGenericPatternsProps() only included
  the Response pattern; added Page pattern inline for the recursive test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tMapping collision

Gap A: when a Mode B genericClass collides with a pre-existing importMapping entry,
log a warning before putIfAbsent so users are aware of the file-vs-import mismatch.

Gap B: add schemaMapping() to the Context interface; implement in SpringCodegen and
KotlinSpringServerCodegen. In preprocessOpenAPI, after re-keying the instance registry,
remove any instance whose raw schemaName is present in schemaMapping() and log a warning.
This prevents genericPatterns from overriding an explicit schemaMapping override.

Test: genericPatterns_schemaMappingOnInstance_skipsSubstitution verifies that a
schema-mapped UserResponse is not substituted to ApiResponse<User> while other
instances (PetResponse) are still substituted normally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add addPreScannedInstance() to GenericSubstitutionSupport accepting a
  GenericInstance and an optional rawMetaSchemaName for companion schema
  suppression. Meta-schemas are suppressed only when their main schema
  was actually removed in the same suppressGenericSchemas() pass.

- Add contributeToGenericSubstitution() to SpringPageableSupport: converts
  each structurally-detected DetectedPagedModel into a GenericInstance and
  registers it via addPreScannedInstance(), giving both features a single
  unified substitution/suppression path.

- Remove pageableSupport.substituteReturnType() from fromOperation() in
  both SpringCodegen and KotlinSpringServerCodegen — handled by the unified
  GenericSubstitutionSupport.substituteReturnType().

- Remove pageableSupport.suppressPagedModels() from postProcessAllModels()
  in both generators — handled by suppressGenericSchemas() which now also
  processes extraSuppressedMetaSchemas.

- Fix toModelImport() in both Context implementations (SpringCodegen,
  KotlinSpringServerCodegen) to return already-FQN names as-is, matching
  AbstractKotlinCodegen.needToImport behaviour. Prevents schema-mapped
  types (e.g. schemaMapping: User → com.example.external.ExternalUser)
  from getting a spurious modelPackage prefix in operation imports.

Gains from unification:
  - Property-level substitution now applies to paged-model schemas
  - isStillReferenced safety check protects paged-model suppression
  - modelNameSuffix/Prefix now work correctly with substituteGenericPagedModel
  - Single import-management flow via syncModelsMapImports

All 86 genericPatterns* + substituteGenericPagedModel* tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tion

Bugs:
- substituteReturnType now preserves outer container (List<UserResponse>
  becomes List<ApiResponse<User>> instead of silently dropping the List).
- extraSuppressedMetaSchemas changed from Map<rawMeta,rawMain> to
  Map<rawMeta,Set<rawMain>>: a meta-schema is suppressed only when ALL
  associated mains were removed. Sibling protection (e.g. one main kept
  via schemaMapping) now correctly keeps the meta.
- isStillReferenced now also checks model.imports, requiredVars,
  optionalVars, and allVars (matching substitutePropertyTypes); used as the
  meta-schema safety check too.
- findRefInProperties: removed dead fallback branch that always returned
  null even when ModelUtils.getReferencedSchema resolved a schema.
- findVaryingRefProperty: skip candidates with absent/non-ref members
  instead of treating null as a distinct ref value.

Readability:
- Dropped dead methods SpringPageableSupport.substituteReturnType /
  suppressPagedModels and their now-unused imports (ModelMap, ModelsMap).
- Updated class Javadocs in GenericSubstitutionSupport, SpringPageableSupport,
  and DetectedPagedModel to describe the current unified architecture.
- Log a warning when toModelName re-keying collapses two raw schema names
  to the same transformed name (previously a silent registry drop).
- Documented substring-collision invariant on the dataType.replace()
  property substitution path.
- autoDetectPagination return value was always discarded; made void.
- GenericSchemaScanUtils: removed dead isArray ternary, unreachable
  return, fixed log placeholder rendering, documented firstTypeArg empty
  case, dropped unused openAPI parameter from findRefInProperties.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Regenerates docs/generators/{spring,kotlin-spring,java-camel}.md to include the genericPatterns and discoverGenericPatterns option rows, and adds a new user guide at docs/customization-genericPatterns.md covering pattern forms, Mode A vs Mode B, vendor-extension overrides, the discovery workflow, and interactions with schemaMapping/importMapping/modelNameSuffix and annotationLibrary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… (Page<T>)

Tier-3 structural cluster discovery previously only considered plain $ref
properties as candidate slots, so it ignored the very common Page<T> /
PagedModel<T> shape where the varying property is an array of $ref
(e.g. content: { type: array, items: { $ref: ... } }).

findVaryingRefProperty now returns a VaryingProperty struct (name + isArray
flag) and considers both plain-$ref and array[$ref] candidate properties.
ClusterSuggestion gains an isArraySlot field (backward-compatible additional
constructor), and buildSuggestedConfig emits `slotArray:` vs `slot:`
accordingly so the suggested YAML is directly usable.

Two new tests cover the happy path (UserPage/PetPage cluster correctly
producing slotArray: content) and the multi-varying rejection path (paged
schemas where both content and metadata refs vary do not cluster). All 178
generic-related tests pass.

The allOf paged-model case remains out of scope for discovery (allOf returns
null fingerprint by design); substituteGenericPagedModel handles that
variant structurally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extends Tier-3 discovery in GenericSchemaScanUtils to cluster allOf-based
schemas (e.g. event envelopes, paged-allOf shapes) in addition to flat
objects.

Mechanics:
* buildStructuralFingerprint now handles allOf with at-most-one inline
  object entry. Fingerprint shape: `allOf|extends:<sorted-refs>|inline:<flat-fp>`,
  with flat-object schemas prefixed `flat|` so the two never collide.
* New helper getCandidateProperties() centralizes `properties for clustering`
  for both flat and allOf shapes; getAllOfExtendsFingerprint() canonicalizes
  the extends-bases.
* findVaryingRefProperty refactored to consume a pre-resolved
  Map<name, props> built once by discoverClusters, so the same code path
  serves flat-object and allOf members.
* allOf members sharing extends-bases but with multi-slot variation, or
  with >1 inline object entries, remain out of scope.

Tests:
* 5 new tests in GenericSchemaScanUtilsTest (event-envelope happy path,
  paged-allOf happy path, different-extends negative, mixed-flat-and-allOf
  negative, two-inline-entries edge case).
* Replaced `allOfSchemas_excludedFromClustering` (now obsolete) with
  the corresponding positive assertion.
* Updated buildStructuralFingerprint_allOfSchema test to verify the
  composite fingerprint shape.

Docs:
* Removed the `allOf-based schemas` exclusion bullet from
  docs/customization-genericPatterns.md; added a note recommending
  substituteGenericPagedModel for pure pagination cases (it remains
  auto-applied and removes the orphaned metadata schemas).

All 183 generics + paged-model tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Picazsoo Picazsoo marked this pull request as ready for review May 31, 2026 22:27
@Picazsoo Picazsoo marked this pull request as draft May 31, 2026 22:28
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 107 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/server/petstore/kotlin-springboot-generics/build.gradle.kts">

<violation number="1" location="samples/server/petstore/kotlin-springboot-generics/build.gradle.kts:3">
P1: Move the `plugins` block to the top of `build.gradle.kts`; Gradle requires it before other build logic, and the current order can break script evaluation.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache:61">
P2: Add a fail-fast configuration check so `min*` cannot be greater than `max*` when both limits are set.</violation>
</file>

<file name="samples/server/petstore/kotlin-springboot-generics/src/main/kotlin/org/openapitools/model/Order.kt">

<violation number="1" location="samples/server/petstore/kotlin-springboot-generics/src/main/kotlin/org/openapitools/model/Order.kt:27">
P2: `totalPrice` should not use `Double` for money values; use a decimal-safe type to avoid precision loss.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Re-trigger cubic

@@ -0,0 +1,48 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

group = "org.openapitools"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Move the plugins block to the top of build.gradle.kts; Gradle requires it before other build logic, and the current order can break script evaluation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/kotlin-springboot-generics/build.gradle.kts, line 3:

<comment>Move the `plugins` block to the top of `build.gradle.kts`; Gradle requires it before other build logic, and the current order can break script evaluation.</comment>

<file context>
@@ -0,0 +1,48 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+group = "org.openapitools"
+version = "1.0.0"
+java.sourceCompatibility = JavaVersion.VERSION_17
</file context>

Comment thread modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache Outdated

@get:JsonProperty("quantity") val quantity: kotlin.Int? = null,

@get:JsonProperty("totalPrice") val totalPrice: kotlin.Double? = null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: totalPrice should not use Double for money values; use a decimal-safe type to avoid precision loss.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/kotlin-springboot-generics/src/main/kotlin/org/openapitools/model/Order.kt, line 27:

<comment>`totalPrice` should not use `Double` for money values; use a decimal-safe type to avoid precision loss.</comment>

<file context>
@@ -0,0 +1,34 @@
+
+    @get:JsonProperty("quantity") val quantity: kotlin.Int? = null,
+
+    @get:JsonProperty("totalPrice") val totalPrice: kotlin.Double? = null
+) : java.io.Serializable {
+
</file context>

maxSize = constraintAnnotation.maxSize
maxPage = constraintAnnotation.maxPage
minSize = constraintAnnotation.minSize
minPage = constraintAnnotation.minPage
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Add a fail-fast configuration check so min* cannot be greater than max* when both limits are set.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-spring/validPageable.mustache, line 61:

<comment>Add a fail-fast configuration check so `min*` cannot be greater than `max*` when both limits are set.</comment>

<file context>
@@ -45,10 +51,14 @@ class PageableConstraintValidator : ConstraintValidator<ValidPageable, Pageable>
         maxSize = constraintAnnotation.maxSize
         maxPage = constraintAnnotation.maxPage
+        minSize = constraintAnnotation.minSize
+        minPage = constraintAnnotation.minPage
     }
 
</file context>

Picazsoo and others added 5 commits June 1, 2026 00:52
1) kotlin-spring dataClass.mustache: container inline enums no longer emit
   `override val value` when useEnumValueInterface=true.

   EnumValueInterfaceUtils.injectInPostProcessModelsEnum() adds
   ValuedEnum<...> to x-kotlin-implements only for `var.isEnum &&
   !var.isContainer`, but the template emitted `override` for every enum.
   Container inline enums therefore generated `override val value` without
   anything to override and failed to compile with "overrides nothing".
   Gating the `override` token on `^isContainer` keeps the template in sync
   with the util.

2) DefaultGenerator + GenericSubstitutionSupport: prevent shared-bundle
   mutations from leaking across supporting-file iterations.

   DefaultGenerator.generateSupportingFiles called
   `config.prepareSupportingFile(bundle, support)` unconditionally — even
   when the file was filtered out by supportingFilesToGenerate or excluded
   by .openapi-generator-ignore. For Mode B generic class files
   (genericClass.mustache) this injected per-file `genericClassDef` into
   the shared bundle that a subsequent iteration would then read.

   Fixed with defense in depth:
   - DefaultGenerator now only calls prepareSupportingFile when the file
     will actually be rendered (shouldGenerate && ignoreProcessor.allowsFile).
   - GenericSubstitutionSupport.prepareSupportingFile now writes the
     "genericClassDef" key unconditionally (null clears stale state), so
     even back-to-back Mode B renders cannot inherit a previous file's
     class data.

Pre-existing test failures (useEnumValueInterface_*, allOf-pageable scan)
are unchanged by these fixes — verified by running the targeted suite on
HEAD before applying the changes.

Sample regen left out of this commit (will be addressed separately) — the
regen also produces drift from unrelated branch state (Kotlin 1.9.25 bump,
Jackson 3 setter nulls, new model additions) that is orthogonal to these
two fixes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The LOGGERS_SHOULD_BE_NOT_PUBLIC_NOT_STATIC_AND_FINAL ArchUnit rule was
violated by static LOGGER fields introduced on this branch:

- GenericSubstitutionSupport (instantiable): make LOGGER an instance field
- SpringPageableSupport (instantiable): make LOGGER an instance field
- GenericSchemaScanUtils (pure static utility): replace static field with a
  private static logger() helper. ArchUnit only checks fields; slf4j caches
  logger lookups, so the indirection is essentially free.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. KotlinSpringServerCodegen: bind useEnumValueInterface from
   additionalProperties in processOpts(). The CLI switch was registered
   via addSwitch() but never read back, so the boolean field stayed false
   and the whole feature was a no-op (no ValuedEnum.kt supporting file,
   no x-kotlin-implements injection, no override val value). Mirrors the
   binding already present in SpringCodegen.processOpts().

2. SpringPageableSupport.processPageableAnnotations(): strip
   x-spring-paginated for non-spring-boot libraries (spring-cloud,
   spring-declarative-http-interface) so the template does not render
   a Pageable parameter for client-side libraries that need explicit
   page/size/sort query parameters. The original strip logic existed
   in 772ff0e but was lost during the pageable-support refactor.

3. SpringPageableSupport.processPageableAnnotations(): include minSize
   and minPage in the @ValidPageable annotation attributes. The
   refactor that moved the annotation builder out of
   SpringPageableScanUtils.applyPageableAnnotations dropped the two
   minimum-bound checks, producing @ValidPageable() with no attributes
   when only a minimum constraint was defined on a paged operation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant