Feature/detect generics allof fix#23917
Conversation
…. Search also allOf references for constraints and defaults
…and minimum bounds
…ested allOf schemas
… handling and enhance annotation application logic
… related documentation
…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>
…ied and therefore dangerous
… allOf-resolved constraints
…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
…ature/detect-generics-allof-fix
… 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>
There was a problem hiding this comment.
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" | |||
There was a problem hiding this comment.
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>
|
|
||
| @get:JsonProperty("quantity") val quantity: kotlin.Int? = null, | ||
|
|
||
| @get:JsonProperty("totalPrice") val totalPrice: kotlin.Double? = null |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
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>
PR checklist
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.
master(upcoming7.x.0minor release - breaking changes with fallbacks),8.0.x(breaking changes without fallbacks)"fixes #123"present in the PR description)Summary by cubic
Add generic schema substitution to
springandkotlin-springto 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 suggestgenericPatternsand updates docs and samples.New Features
genericPatterns: configure suffix/prefix patterns withslot/slotArrayto substitute return and property types; safely suppress redundant schemas; works withschemaMapping,importMapping, andmodelNamePrefix/modelNameSuffix.discoverGenericPatterns: scan specs and log ready-to-copy suggestions (supports arrays likePage<T>andallOf-based shapes).UserPage→Page<User>), with safe removal of orphaned meta-schemas.useEnumValueInterface: generate aValuedEnum<T>inconfigPackageand have all enums implement it; override viaimportMappings.ValuedEnum.prepareSupportingFile(bundle, file)to adjust per-file data. New docs atdocs/customization-genericPatterns.md; sample configs and projects added for both generators.Bug Fixes
List<UserResponse>→List<ApiResponse<User>>) and correctly handle array properties and imports.modelNamePrefix/modelNameSuffixis set; honor explicitschemaMappingoverrides.minSize/minPage; resolve min/max and defaults through nestedallOf; apply annotations consistently; limit Pageable features to Spring Boot libraries and update docs.override val valuefor container inline enums whenuseEnumValueInterfaceis on (fixes “overrides nothing”); binduseEnumValueInterfacefromadditionalPropertiesso the feature actually generatesValuedEnumand implements it.prepareSupportingFilefor files that will render and reset per-file data to prevent leakage between supporting files (stabilizes Mode BgenericClassgeneration).Written for commit 7c9a072. Summary will update on new commits.