Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 @@ -200,6 +200,8 @@ public String getDescription() {

// Map from schema name to detected paged-model info (populated when substituteGenericPagedModel=true)
private Map<String, PagedModelScanUtils.DetectedPagedModel> pagedModelRegistry = new HashMap<>();
// Simple class name of the PagedModel substitute (derived from importMapping; defaults to "PagedModel")
private String pagedModelClassName = "PagedModel";

public KotlinSpringServerCodegen() {
super();
Expand Down Expand Up @@ -1142,21 +1144,24 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
pagedModelRegistry.get(codegenOperation.returnBaseType);
if (detected != null) {
String oldType = codegenOperation.returnType;
String newBaseType = "PagedModel<" + detected.itemSchemaName + ">";
// Run through toModelName so that schemaMappings (e.g. User → com.example.MyUser)
// are honored: the mapped name is used both in the type arg and for import resolution.
String itemType = toModelName(detected.itemSchemaName);
String newBaseType = pagedModelClassName + "<" + itemType + ">";
codegenOperation.returnType = newBaseType;
codegenOperation.returnBaseType = "PagedModel";
codegenOperation.returnBaseType = pagedModelClassName;
// Clear any container flag — PagedModel is not itself a List/array
codegenOperation.returnContainer = null;
// Add item type import (needed for PagedModel<User> in method signature)
codegenOperation.imports.add(detected.itemSchemaName);
codegenOperation.imports.add("PagedModel");
// Add item type import (needed for PagedModel<T> in method signature)
codegenOperation.imports.add(itemType);
codegenOperation.imports.add(pagedModelClassName);
// Remove paged schema import when no annotations are generated —
// the class is suppressed and not referenced anywhere
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
codegenOperation.imports.remove(detected.schemaName);
}
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>",
codegenOperation.operationId, oldType, detected.itemSchemaName);
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with {}<{}>",
codegenOperation.operationId, oldType, pagedModelClassName, itemType);
}
}

Expand Down Expand Up @@ -1202,7 +1207,22 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
if (SPRING_BOOT.equals(library) && substituteGenericPagedModel) {
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
if (!pagedModelRegistry.isEmpty()) {
importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel");
boolean customMapping = importMapping.containsKey("PagedModel");
importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel");
if (!customMapping) {
// No custom class provided — generate the simple PagedModel into the config package.
supportingFiles.add(new SupportingFile("pagedModel.mustache",
(sourceFolder + File.separator + configPackage).replace(".", File.separator), "PagedModel.kt"));
}
// Derive the actual simple class name from the FQN in importMapping so that a
// custom mapping (e.g. "PagedModel" → "com.example.MyPagedModel") is respected.
// The simple name of the FQN becomes the token used in generated code, and is
// registered in importMapping so that template import resolution works.
String fqn = importMapping.get("PagedModel");
pagedModelClassName = fqn.substring(fqn.lastIndexOf('.') + 1);
if (!pagedModelClassName.equals("PagedModel")) {
importMapping.put(pagedModelClassName, fqn);
}
LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}",
pagedModelRegistry.size(), pagedModelRegistry.keySet());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ public enum RequestMappingMode {
private Map<String, SpringPageableScanUtils.PageableConstraintsData> pageableConstraintsRegistry = new HashMap<>();
// Map from schema name to detected paged-model info (populated when substituteGenericPagedModel=true)
private Map<String, PagedModelScanUtils.DetectedPagedModel> pagedModelRegistry = new HashMap<>();
// Simple class name of the PagedModel substitute (derived from importMapping; defaults to "PagedModel")
private String pagedModelClassName = "PagedModel";

public SpringCodegen() {
super();
Expand Down Expand Up @@ -870,7 +872,22 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
if (SPRING_BOOT.equals(library) && substituteGenericPagedModel) {
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
if (!pagedModelRegistry.isEmpty()) {
importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel");
boolean customMapping = importMapping.containsKey("PagedModel");
importMapping.putIfAbsent("PagedModel", configPackage + ".PagedModel");
if (!customMapping) {
// No custom class provided — generate the simple PagedModel into the config package.
supportingFiles.add(new SupportingFile("pagedModel.mustache",
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "PagedModel.java"));
}
// Derive the actual simple class name from the FQN in importMapping so that a
// custom mapping (e.g. "PagedModel" → "com.example.MyPagedModel") is respected.
// The simple name of the FQN becomes the token used in generated code, and is
// registered in importMapping so that template import resolution works.
String fqn = importMapping.get("PagedModel");
pagedModelClassName = fqn.substring(fqn.lastIndexOf('.') + 1);
if (!pagedModelClassName.equals("PagedModel")) {
importMapping.put(pagedModelClassName, fqn);
}
LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}",
pagedModelRegistry.size(), pagedModelRegistry.keySet());
}
Expand Down Expand Up @@ -1352,21 +1369,24 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
pagedModelRegistry.get(codegenOperation.returnBaseType);
if (detected != null) {
String oldType = codegenOperation.returnType;
String newBaseType = "PagedModel<" + detected.itemSchemaName + ">";
// Run through toModelName so that schemaMappings (e.g. User → com.example.MyUser)
// are honored: the mapped name is used both in the type arg and for import resolution.
String itemType = toModelName(detected.itemSchemaName);
String newBaseType = pagedModelClassName + "<" + itemType + ">";
codegenOperation.returnType = newBaseType;
codegenOperation.returnBaseType = "PagedModel";
codegenOperation.returnBaseType = pagedModelClassName;
// Clear any container flag — PagedModel is not itself a List/array
codegenOperation.returnContainer = null;
// Add item type import (needed for PagedModel<User> in method signature)
codegenOperation.imports.add(detected.itemSchemaName);
codegenOperation.imports.add("PagedModel");
// Add item type import (needed for PagedModel<T> in method signature)
codegenOperation.imports.add(itemType);
codegenOperation.imports.add(pagedModelClassName);
// Remove paged schema import when no annotations are generated —
// the class is suppressed and not referenced anywhere
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
codegenOperation.imports.remove(detected.schemaName);
}
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>",
codegenOperation.operationId, oldType, detected.itemSchemaName);
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with {}<{}>",
codegenOperation.operationId, oldType, pagedModelClassName, itemType);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package {{configPackage}};

import java.util.List;

/**
* Simple generic paged response wrapper generated by openapi-generator.
*
* <p>Holds a page of content items and pagination metadata. Jackson deserializes this from the
* standard Spring {@code Page} JSON shape:
* <pre>
* {
* "content": [...],
* "page": { "size": 20, "totalElements": 100, "totalPages": 5, "number": 0 }
* }
* </pre>
*
* <p>To use your own class instead, set {@code importMappings.PagedModel} in the generator config.
*/
public class PagedModel<T> {

private List<T> content;
private PageMetadata page;

public PagedModel() {}

public PagedModel(List<T> content, PageMetadata page) {
this.content = content;
this.page = page;
}

public List<T> getContent() {
return content;
}

public void setContent(List<T> content) {
this.content = content;
}

public PageMetadata getPage() {
return page;
}

public void setPage(PageMetadata page) {
this.page = page;
}

public static class PageMetadata {

private long size;
private long totalElements;
private long totalPages;
private long number;

public PageMetadata() {}

public PageMetadata(long size, long totalElements, long totalPages, long number) {
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.number = number;
}

public long getSize() {
return size;
}

public void setSize(long size) {
this.size = size;
}

public long getTotalElements() {
return totalElements;
}

public void setTotalElements(long totalElements) {
this.totalElements = totalElements;
}

public long getTotalPages() {
return totalPages;
}

public void setTotalPages(long totalPages) {
this.totalPages = totalPages;
}

public long getNumber() {
return number;
}

public void setNumber(long number) {
this.number = number;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package {{configPackage}}

/**
* Simple generic paged response wrapper generated by openapi-generator.
*
* Holds a page of content items and pagination metadata. Jackson deserializes this from the
* standard Spring `Page` JSON shape:
* ```json
* {
* "content": [...],
* "page": { "size": 20, "totalElements": 100, "totalPages": 5, "number": 0 }
* }
* ```
*
* To use your own class instead, set `importMappings.PagedModel` in the generator config.
*/
data class PagedModel<T>(
val content: List<T>,
val page: PageMetadata,
) {
data class PageMetadata(
val size: Long,
val totalElements: Long,
val totalPages: Long,
val number: Long,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7242,9 +7242,9 @@ public void substituteGenericPagedModel_importsPagedModelAndItemTypeInApiFile()
Map<String, File> files = generateFromContract(
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props);

// The api file must import both PagedModel and the item type
// The api file must import both the generated PagedModel and the item type
JavaFileAssert.assertThat(files.get("UserApi.java"))
.fileContains("import org.springframework.data.web.PagedModel")
.fileContains("import org.openapitools.configuration.PagedModel")
.fileContains("import org.openapitools.model.User");
}

Expand Down Expand Up @@ -7302,4 +7302,75 @@ public void substituteGenericPagedModel_suppressesPageMetaWhenNoAnnotations() th
// PageMeta is referenced by SearchResult (a non-paged schema) → must be kept
assertThat(files).containsKey("PageMeta.java");
}

@Test
public void substituteGenericPagedModel_respectsSchemaMappingForItemType() throws IOException {
// When the item schema (User) is mapped to an external FQN via schemaMappings,
// the PagedModel type arg must use the mapped FQN, not the raw schema name.
Map<String, Object> props = commonPagedModelProps();

Map<String, File> files = generateFromContract(
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props,
configurator -> configurator.addSchemaMapping("User", "com.example.external.ExternalUser"));

JavaFileAssert.assertThat(files.get("UserApi.java"))
.assertMethod("listUsers")
.hasReturnType("ResponseEntity<PagedModel<com.example.external.ExternalUser>>");
}

@Test
public void substituteGenericPagedModel_respectsSchemaMappingWithImportMappingForItemType() throws IOException {
// When the item schema (User) is mapped to an external FQN via schemaMappings,
// the PagedModel type arg must use the mapped FQN, not the raw schema name.
Map<String, Object> props = commonPagedModelProps();

Map<String, File> files = generateFromContract(
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props,
configurator -> configurator
.addSchemaMapping("User", "ExternalUser")
.addImportMapping("ExternalUser", "com.example.external.ExternalUser"));

JavaFileAssert.assertThat(files.get("UserApi.java"))
.hasImports("com.example.external.ExternalUser")
.assertMethod("listUsers")
.hasReturnType("ResponseEntity<PagedModel<ExternalUser>>");
}

@Test
public void substituteGenericPagedModel_generatesPagedModelSupportingFile() throws IOException {
Map<String, File> files = generateFromContract(
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, commonPagedModelProps());

assertThat(files).containsKey("PagedModel.java");
}

@Test
public void substituteGenericPagedModel_doesNotGeneratePagedModelFileWhenCustomMapping() throws IOException {
Map<String, Object> props = commonPagedModelProps();

Map<String, File> files = generateFromContract(
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props,
configurator -> configurator
.addImportMapping("PagedModel", "com.example.custom.MyPagedModel"));

assertThat(files).doesNotContainKey("PagedModel.java");
}

@Test
public void substituteGenericPagedModel_respectsCustomImportMappingClassName() throws IOException {
// When the user remaps "PagedModel" to a FQN with a different simple class name,
// the generated code must use that simple name (not "PagedModel") as the type token
// and emit the correct import for the custom FQN.
Map<String, Object> props = commonPagedModelProps();

Map<String, File> files = generateFromContract(
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props,
configurator -> configurator
.addImportMapping("PagedModel", "com.example.custom.MyPagedModel"));

JavaFileAssert.assertThat(files.get("UserApi.java"))
.hasImports("com.example.custom.MyPagedModel")
.assertMethod("listUsers")
.hasReturnType("ResponseEntity<MyPagedModel<User>>");
}
}
Loading
Loading