Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
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
4f7a6dc
Merge branch 'master' into bugfix/pageable-scan-resolve-allof-constra…
Picazsoo May 30, 2026
2bdcb2f
fix(kotlin-spring): container inline enums must not get `override val…
Picazsoo May 31, 2026
48d7411
feat(kotlin-spring and java-spring): add ValuedEnum interface and upd…
Picazsoo Jun 3, 2026
ae4ed18
Merge branch 'master' into bugfix/pageable-scan-resolve-allof-constra…
Picazsoo Jun 3, 2026
97d629b
update samples
Picazsoo Jun 3, 2026
328d83f
update samples
Picazsoo Jun 3, 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
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
List<String> attrs = new ArrayList<>();
if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize);
if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage);
if (constraints.minSize >= 0) attrs.add("minSize = " + constraints.minSize);
if (constraints.minPage >= 0) attrs.add("minPage = " + constraints.minPage);
pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")");
codegenOperation.imports.add("ValidPageable");
}
Expand Down Expand Up @@ -1212,7 +1214,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
}

if (substituteGenericPagedModel) {
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI, this::toModelName);
if (!pagedModelRegistry.isEmpty()) {
boolean customMapping = importMapping.containsKey("PagedModel");
importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel");
Expand Down Expand Up @@ -1383,32 +1385,35 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
// No @ApiResponse annotations are generated when annotationLibrary=none,
// so paged schemas are not referenced anywhere → safe to suppress.
Set<String> metaSchemasToCheck = new HashSet<>();
// metaSchemasToCheck maps transformed name (for imports check) → raw name (for objs.remove)
Map<String, String> metaSchemasToCheck = new LinkedHashMap<>();
for (PagedModelScanUtils.DetectedPagedModel detected : pagedModelRegistry.values()) {
if (detected.metaSchemaName != null) {
metaSchemasToCheck.add(detected.metaSchemaName);
metaSchemasToCheck.put(detected.metaSchemaName, detected.rawMetaSchemaName);
}
}
// Remove paged schemas first so reference checks below reflect the post-suppression state.
for (Map.Entry<String, PagedModelScanUtils.DetectedPagedModel> entry : pagedModelRegistry.entrySet()) {
String schemaName = entry.getKey();
PagedModelScanUtils.DetectedPagedModel detected = entry.getValue();
if (objs.remove(schemaName) != null) {
// objs is keyed by raw schema name (DefaultGenerator uses the raw OpenAPI name as key)
if (objs.remove(detected.rawSchemaName) != null) {
LOGGER.info("substituteGenericPagedModel: suppressing model '{}' — replaced by PagedModel<{}>",
schemaName, detected.itemSchemaName);
detected.rawSchemaName, detected.itemSchemaName);
}
}
// Suppress meta schemas only when no remaining (non-suppressed) schema references them.
// Example: if SearchResult has a 'page: PageMeta' property, PageMeta must be kept.
for (String metaName : metaSchemasToCheck) {
for (Map.Entry<String, String> metaEntry : metaSchemasToCheck.entrySet()) {
String metaName = metaEntry.getKey(); // transformed — matches cm.imports values
String rawMetaName = metaEntry.getValue(); // raw — matches objs key
boolean referencedElsewhere = objs.values().stream()
.flatMap(mm -> mm.getModels().stream())
.map(ModelMap::getModel)
.anyMatch(cm -> cm.imports.contains(metaName));
if (referencedElsewhere) {
LOGGER.info("substituteGenericPagedModel: keeping pagination metadata model '{}'"
+ " — referenced by a non-paged schema", metaName);
} else if (objs.remove(metaName) != null) {
} else if (objs.remove(rawMetaName) != null) {
LOGGER.info("substituteGenericPagedModel: suppressing pagination metadata model '{}'"
+ " — replaced by PagedModel.PageMetadata", metaName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.openapitools.codegen.utils.ModelUtils;

import java.util.*;
import java.util.function.UnaryOperator;

/**
* Language-agnostic utility for detecting OpenAPI schemas that represent paginated responses
Expand Down Expand Up @@ -61,21 +62,56 @@ private PagedModelScanUtils() {}
/**
* Carries the result of a single detected paged-model schema.
*
* @param schemaName Name of the detected schema to suppress (e.g. {@code UserPage}).
* @param itemSchemaName Simple name of the array item type (e.g. {@code User}).
* @param metaSchemaName Name of the pagination-metadata schema to suppress
* (e.g. {@code PageMetadata}), or {@code null} if it could not
* be resolved to a named component.
* <p>Two name variants are stored for each schema:</p>
* <ul>
* <li><b>transformed</b> ({@code schemaName} / {@code metaSchemaName}) — the model name
* after the generator's {@code toModelName()} has been applied. These are the names
* that appear in codegen-operation imports and {@code CodegenModel.imports}, so they
* must be used for import removal / import-presence checks.</li>
* <li><b>raw</b> ({@code rawSchemaName} / {@code rawMetaSchemaName}) — the original
* OpenAPI component-schema name. {@code DefaultGenerator} keys {@code allProcessedModels}
* (the {@code objs} map passed to {@code postProcessAllModels}) by the <em>raw</em>
* schema name, so these values must be used for {@code objs.remove()} calls.</li>
* </ul>
*
* <p>When {@link #scanPagedModels(OpenAPI)} is used (no transform), the raw and transformed
* names are identical. When {@link #scanPagedModels(OpenAPI, UnaryOperator)} is used, they
* may differ (e.g. {@code rawSchemaName="UserPage"}, {@code schemaName="UserPageDto"}).</p>
*
* @param schemaName Transformed model name of the detected paged schema.
* @param itemSchemaName Raw item schema name (always raw; callers apply
* {@code toModelName()} at the point of use).
* @param metaSchemaName Transformed model name of the pagination-metadata schema,
* or {@code null} if unresolved.
* @param rawSchemaName Raw OpenAPI schema name of the paged schema (for {@code objs.remove}).
* @param rawMetaSchemaName Raw OpenAPI schema name of the pagination-metadata schema
* (for {@code objs.remove}), or {@code null} if unresolved.
*/
public static final class DetectedPagedModel {
/** Transformed model name — use for import removal / import-presence checks. */
public final String schemaName;
public final String itemSchemaName;
/** Transformed meta model name — use for import-presence checks. */
public final String metaSchemaName;

/** Raw OpenAPI schema name — use for {@code objs.remove()} in {@code postProcessAllModels}. */
public final String rawSchemaName;
/** Raw OpenAPI meta schema name — use for {@code objs.remove()} in {@code postProcessAllModels}. */
public final String rawMetaSchemaName;

/**
* Convenience constructor used when no name transform is active (raw == transformed).
*/
public DetectedPagedModel(String schemaName, String itemSchemaName, String metaSchemaName) {
this(schemaName, itemSchemaName, metaSchemaName, schemaName, metaSchemaName);
}

DetectedPagedModel(String schemaName, String itemSchemaName, String metaSchemaName,
String rawSchemaName, String rawMetaSchemaName) {
this.schemaName = schemaName;
this.itemSchemaName = itemSchemaName;
this.metaSchemaName = metaSchemaName;
this.rawSchemaName = rawSchemaName;
this.rawMetaSchemaName = rawMetaSchemaName;
}
}

Expand Down Expand Up @@ -112,7 +148,43 @@ public static Map<String, DetectedPagedModel> scanPagedModels(OpenAPI openAPI) {
}

/**
* Returns {@code true} if the given schema looks like a pagination-metadata schema.
* Convenience overload that scans for paged-model schemas and immediately re-keys the
* resulting map by applying {@code toModelName} to every schema name.
*
* <p>Generator classes must use this overload (passing {@code this::toModelName}) so that
* the registry keys match the model-name-processed values used at lookup time
* (e.g. {@code codegenOperation.returnBaseType}, {@code objs} keys). This ensures
* correctness when {@code modelNameSuffix}, {@code modelNamePrefix}, {@code schemaMapping},
* or {@code modelNameMapping} are active.</p>
*
* <p>{@code itemSchemaName} inside each {@link DetectedPagedModel} is intentionally left as
* the raw spec name because every call site already passes it through {@code toModelName()}
* at the point of use.</p>
*
* @param openAPI the parsed OpenAPI document
* @param toModelName name-transformation function supplied by the generator
* (typically {@code this::toModelName})
* @return map from transformed schema name to {@link DetectedPagedModel}
*/
public static Map<String, DetectedPagedModel> scanPagedModels(
OpenAPI openAPI, UnaryOperator<String> toModelName) {
Map<String, DetectedPagedModel> raw = scanPagedModels(openAPI);
if (raw.isEmpty()) {
return raw;
}
Map<String, DetectedPagedModel> result = new LinkedHashMap<>();
for (Map.Entry<String, DetectedPagedModel> entry : raw.entrySet()) {
DetectedPagedModel d = entry.getValue();
String rawKey = entry.getKey();
String newKey = toModelName.apply(rawKey);
String rawMeta = d.metaSchemaName;
String newMeta = rawMeta != null ? toModelName.apply(rawMeta) : null;
result.put(newKey, new DetectedPagedModel(newKey, d.itemSchemaName, newMeta, rawKey, rawMeta));
}
return result;
}

/**
*
* <p>The heuristic checks that at least {@value #PAGINATION_FIELD_THRESHOLD} of the
* well-known field names ({@code size}, {@code number}, {@code page},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
}

if (substituteGenericPagedModel) {
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI, this::toModelName);
if (!pagedModelRegistry.isEmpty()) {
boolean customMapping = importMapping.containsKey("PagedModel");
importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel");
Expand Down Expand Up @@ -1272,6 +1272,8 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
List<String> attrs = new ArrayList<>();
if (constraints.maxSize >= 0) attrs.add("maxSize = " + constraints.maxSize);
if (constraints.maxPage >= 0) attrs.add("maxPage = " + constraints.maxPage);
if (constraints.minSize >= 0) attrs.add("minSize = " + constraints.minSize);
if (constraints.minPage >= 0) attrs.add("minPage = " + constraints.minPage);
pageableAnnotations.add("@ValidPageable(" + String.join(", ", attrs) + ")");
codegenOperation.imports.add("ValidPageable");
}
Expand Down Expand Up @@ -1450,32 +1452,35 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
// No @ApiResponse annotations are generated when annotationLibrary=none,
// so paged schemas are not referenced anywhere → safe to suppress.
Set<String> metaSchemasToCheck = new HashSet<>();
// metaSchemasToCheck maps transformed name (for imports check) → raw name (for objs.remove)
Map<String, String> metaSchemasToCheck = new LinkedHashMap<>();
for (PagedModelScanUtils.DetectedPagedModel detected : pagedModelRegistry.values()) {
if (detected.metaSchemaName != null) {
metaSchemasToCheck.add(detected.metaSchemaName);
metaSchemasToCheck.put(detected.metaSchemaName, detected.rawMetaSchemaName);
}
}
// Remove paged schemas first so reference checks below reflect the post-suppression state.
for (Map.Entry<String, PagedModelScanUtils.DetectedPagedModel> entry : pagedModelRegistry.entrySet()) {
String schemaName = entry.getKey();
PagedModelScanUtils.DetectedPagedModel detected = entry.getValue();
if (objs.remove(schemaName) != null) {
// objs is keyed by raw schema name (DefaultGenerator uses the raw OpenAPI name as key)
if (objs.remove(detected.rawSchemaName) != null) {
LOGGER.info("substituteGenericPagedModel: suppressing model '{}' — replaced by PagedModel<{}>",
schemaName, detected.itemSchemaName);
detected.rawSchemaName, detected.itemSchemaName);
}
}
// Suppress meta schemas only when no remaining (non-suppressed) schema references them.
// Example: if SearchResult has a 'page: PageMeta' property, PageMeta must be kept.
for (String metaName : metaSchemasToCheck) {
for (Map.Entry<String, String> metaEntry : metaSchemasToCheck.entrySet()) {
String metaName = metaEntry.getKey(); // transformed — matches cm.imports values
String rawMetaName = metaEntry.getValue(); // raw — matches objs key
boolean referencedElsewhere = objs.values().stream()
.flatMap(mm -> mm.getModels().stream())
.map(ModelMap::getModel)
.anyMatch(cm -> cm.imports.contains(metaName));
if (referencedElsewhere) {
LOGGER.info("substituteGenericPagedModel: keeping pagination metadata model '{}'"
+ " — referenced by a non-paged schema", metaName);
} else if (objs.remove(metaName) != null) {
} else if (objs.remove(rawMetaName) != null) {
LOGGER.info("substituteGenericPagedModel: suppressing pagination metadata model '{}'"
+ " — replaced by PagedModel.PageMetadata", metaName);
}
Expand Down
Loading
Loading