Skip to content

Commit f7c827a

Browse files
committed
fix(spring): ensure PagedModel uses mapped FQN for item type in code generation
fix(spring): enhance PagedModel handling with import mapping and schema mapping support
1 parent f1935af commit f7c827a

4 files changed

Lines changed: 131 additions & 14 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ public String getDescription() {
200200

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

204206
public KotlinSpringServerCodegen() {
205207
super();
@@ -1142,21 +1144,24 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
11421144
pagedModelRegistry.get(codegenOperation.returnBaseType);
11431145
if (detected != null) {
11441146
String oldType = codegenOperation.returnType;
1145-
String newBaseType = "PagedModel<" + detected.itemSchemaName + ">";
1147+
// Run through toModelName so that schemaMappings (e.g. User → com.example.MyUser)
1148+
// are honored: the mapped name is used both in the type arg and for import resolution.
1149+
String itemType = toModelName(detected.itemSchemaName);
1150+
String newBaseType = pagedModelClassName + "<" + itemType + ">";
11461151
codegenOperation.returnType = newBaseType;
1147-
codegenOperation.returnBaseType = "PagedModel";
1152+
codegenOperation.returnBaseType = pagedModelClassName;
11481153
// Clear any container flag — PagedModel is not itself a List/array
11491154
codegenOperation.returnContainer = null;
1150-
// Add item type import (needed for PagedModel<User> in method signature)
1151-
codegenOperation.imports.add(detected.itemSchemaName);
1152-
codegenOperation.imports.add("PagedModel");
1155+
// Add item type import (needed for PagedModel<T> in method signature)
1156+
codegenOperation.imports.add(itemType);
1157+
codegenOperation.imports.add(pagedModelClassName);
11531158
// Remove paged schema import when no annotations are generated —
11541159
// the class is suppressed and not referenced anywhere
11551160
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
11561161
codegenOperation.imports.remove(detected.schemaName);
11571162
}
1158-
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>",
1159-
codegenOperation.operationId, oldType, detected.itemSchemaName);
1163+
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with {}<{}>",
1164+
codegenOperation.operationId, oldType, pagedModelClassName, itemType);
11601165
}
11611166
}
11621167

@@ -1203,6 +1208,15 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
12031208
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
12041209
if (!pagedModelRegistry.isEmpty()) {
12051210
importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel");
1211+
// Derive the actual simple class name from the FQN in importMapping so that a
1212+
// custom mapping (e.g. "PagedModel" → "com.example.MyPagedModel") is respected.
1213+
// The simple name of the FQN becomes the token used in generated code, and is
1214+
// registered in importMapping so that template import resolution works.
1215+
String fqn = importMapping.get("PagedModel");
1216+
pagedModelClassName = fqn.substring(fqn.lastIndexOf('.') + 1);
1217+
if (!pagedModelClassName.equals("PagedModel")) {
1218+
importMapping.put(pagedModelClassName, fqn);
1219+
}
12061220
LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}",
12071221
pagedModelRegistry.size(), pagedModelRegistry.keySet());
12081222
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ public enum RequestMappingMode {
203203
private Map<String, SpringPageableScanUtils.PageableConstraintsData> pageableConstraintsRegistry = new HashMap<>();
204204
// Map from schema name to detected paged-model info (populated when substituteGenericPagedModel=true)
205205
private Map<String, PagedModelScanUtils.DetectedPagedModel> pagedModelRegistry = new HashMap<>();
206+
// Simple class name of the PagedModel substitute (derived from importMapping; defaults to "PagedModel")
207+
private String pagedModelClassName = "PagedModel";
206208

207209
public SpringCodegen() {
208210
super();
@@ -871,6 +873,15 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
871873
pagedModelRegistry = PagedModelScanUtils.scanPagedModels(openAPI);
872874
if (!pagedModelRegistry.isEmpty()) {
873875
importMapping.putIfAbsent("PagedModel", "org.springframework.data.web.PagedModel");
876+
// Derive the actual simple class name from the FQN in importMapping so that a
877+
// custom mapping (e.g. "PagedModel" → "com.example.MyPagedModel") is respected.
878+
// The simple name of the FQN becomes the token used in generated code, and is
879+
// registered in importMapping so that template import resolution works.
880+
String fqn = importMapping.get("PagedModel");
881+
pagedModelClassName = fqn.substring(fqn.lastIndexOf('.') + 1);
882+
if (!pagedModelClassName.equals("PagedModel")) {
883+
importMapping.put(pagedModelClassName, fqn);
884+
}
874885
LOGGER.info("substituteGenericPagedModel: detected {} paged-model schema(s): {}",
875886
pagedModelRegistry.size(), pagedModelRegistry.keySet());
876887
}
@@ -1352,21 +1363,24 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
13521363
pagedModelRegistry.get(codegenOperation.returnBaseType);
13531364
if (detected != null) {
13541365
String oldType = codegenOperation.returnType;
1355-
String newBaseType = "PagedModel<" + detected.itemSchemaName + ">";
1366+
// Run through toModelName so that schemaMappings (e.g. User → com.example.MyUser)
1367+
// are honored: the mapped name is used both in the type arg and for import resolution.
1368+
String itemType = toModelName(detected.itemSchemaName);
1369+
String newBaseType = pagedModelClassName + "<" + itemType + ">";
13561370
codegenOperation.returnType = newBaseType;
1357-
codegenOperation.returnBaseType = "PagedModel";
1371+
codegenOperation.returnBaseType = pagedModelClassName;
13581372
// Clear any container flag — PagedModel is not itself a List/array
13591373
codegenOperation.returnContainer = null;
1360-
// Add item type import (needed for PagedModel<User> in method signature)
1361-
codegenOperation.imports.add(detected.itemSchemaName);
1362-
codegenOperation.imports.add("PagedModel");
1374+
// Add item type import (needed for PagedModel<T> in method signature)
1375+
codegenOperation.imports.add(itemType);
1376+
codegenOperation.imports.add(pagedModelClassName);
13631377
// Remove paged schema import when no annotations are generated —
13641378
// the class is suppressed and not referenced anywhere
13651379
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
13661380
codegenOperation.imports.remove(detected.schemaName);
13671381
}
1368-
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with PagedModel<{}>",
1369-
codegenOperation.operationId, oldType, detected.itemSchemaName);
1382+
LOGGER.info("substituteGenericPagedModel: operation '{}': replacing return type '{}' with {}<{}>",
1383+
codegenOperation.operationId, oldType, pagedModelClassName, itemType);
13701384
}
13711385
}
13721386

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7302,4 +7302,55 @@ public void substituteGenericPagedModel_suppressesPageMetaWhenNoAnnotations() th
73027302
// PageMeta is referenced by SearchResult (a non-paged schema) → must be kept
73037303
assertThat(files).containsKey("PageMeta.java");
73047304
}
7305+
7306+
@Test
7307+
public void substituteGenericPagedModel_respectsSchemaMappingForItemType() throws IOException {
7308+
// When the item schema (User) is mapped to an external FQN via schemaMappings,
7309+
// the PagedModel type arg must use the mapped FQN, not the raw schema name.
7310+
Map<String, Object> props = commonPagedModelProps();
7311+
7312+
Map<String, File> files = generateFromContract(
7313+
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props,
7314+
configurator -> configurator.addSchemaMapping("User", "com.example.external.ExternalUser"));
7315+
7316+
JavaFileAssert.assertThat(files.get("UserApi.java"))
7317+
.assertMethod("listUsers")
7318+
.hasReturnType("ResponseEntity<PagedModel<com.example.external.ExternalUser>>");
7319+
}
7320+
7321+
@Test
7322+
public void substituteGenericPagedModel_respectsSchemaMappingWithImportMappingForItemType() throws IOException {
7323+
// When the item schema (User) is mapped to an external FQN via schemaMappings,
7324+
// the PagedModel type arg must use the mapped FQN, not the raw schema name.
7325+
Map<String, Object> props = commonPagedModelProps();
7326+
7327+
Map<String, File> files = generateFromContract(
7328+
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props,
7329+
configurator -> configurator
7330+
.addSchemaMapping("User", "ExternalUser")
7331+
.addImportMapping("ExternalUser", "com.example.external.ExternalUser"));
7332+
7333+
JavaFileAssert.assertThat(files.get("UserApi.java"))
7334+
.hasImports("com.example.external.ExternalUser")
7335+
.assertMethod("listUsers")
7336+
.hasReturnType("ResponseEntity<PagedModel<ExternalUser>>");
7337+
}
7338+
7339+
@Test
7340+
public void substituteGenericPagedModel_respectsCustomImportMappingClassName() throws IOException {
7341+
// When the user remaps "PagedModel" to a FQN with a different simple class name,
7342+
// the generated code must use that simple name (not "PagedModel") as the type token
7343+
// and emit the correct import for the custom FQN.
7344+
Map<String, Object> props = commonPagedModelProps();
7345+
7346+
Map<String, File> files = generateFromContract(
7347+
"src/test/resources/3_0/spring/petstore-paged-model.yaml", SPRING_BOOT, props,
7348+
configurator -> configurator
7349+
.addImportMapping("PagedModel", "com.example.custom.MyPagedModel"));
7350+
7351+
JavaFileAssert.assertThat(files.get("UserApi.java"))
7352+
.hasImports("com.example.custom.MyPagedModel")
7353+
.assertMethod("listUsers")
7354+
.hasReturnType("ResponseEntity<MyPagedModel<User>>");
7355+
}
73057356
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5662,4 +5662,42 @@ public void substituteGenericPagedModel_suppressesPageMetaWhenNoAnnotations() th
56625662
// PageMeta is referenced by SearchResult (a non-paged schema) → must be kept
56635663
assertThat(files).containsKey("PageMeta.kt");
56645664
}
5665+
5666+
@Test
5667+
public void substituteGenericPagedModel_respectsSchemaMappingForItemType() throws IOException {
5668+
// When the item schema (User) is mapped to an external FQN via schemaMappings,
5669+
// the PagedModel type arg must use the mapped FQN, not the raw schema name.
5670+
Map<String, File> files = generateFromContract(
5671+
"src/test/resources/3_0/spring/petstore-paged-model.yaml",
5672+
commonKotlinPagedModelProps(),
5673+
new HashMap<>(),
5674+
configurator -> configurator.addSchemaMapping("User", "com.example.external.ExternalUser"));
5675+
5676+
File userApi = files.get("UserApi.kt");
5677+
assertThat(userApi).isNotNull();
5678+
String content = Files.readString(userApi.toPath());
5679+
// Return type must use the schema-mapped FQN, not the raw schema name
5680+
assertThat(content).contains("PagedModel<com.example.external.ExternalUser>");
5681+
// toModelImport of a dotted name returns the FQN as-is → correct import
5682+
assertThat(content).contains("import com.example.external.ExternalUser");
5683+
}
5684+
5685+
@Test
5686+
public void substituteGenericPagedModel_respectsCustomImportMappingClassName() throws IOException {
5687+
// When the user remaps "PagedModel" to a FQN with a different simple class name,
5688+
// the generated code must use that simple name (not "PagedModel") as the type token
5689+
// and emit the correct import for the custom FQN.
5690+
Map<String, File> files = generateFromContract(
5691+
"src/test/resources/3_0/spring/petstore-paged-model.yaml",
5692+
commonKotlinPagedModelProps(),
5693+
new HashMap<>(),
5694+
configurator -> configurator
5695+
.addImportMapping("PagedModel", "com.example.custom.MyPagedModel"));
5696+
5697+
File userApi = files.get("UserApi.kt");
5698+
assertThat(userApi).isNotNull();
5699+
String content = Files.readString(userApi.toPath());
5700+
assertThat(content).contains("MyPagedModel<User>");
5701+
assertThat(content).contains("import com.example.custom.MyPagedModel");
5702+
}
56655703
}

0 commit comments

Comments
 (0)