@@ -111,6 +111,9 @@ public class SpringCodegen extends AbstractJavaCodegen
111111 public static final String JACKSON3_PACKAGE = "tools.jackson" ;
112112 public static final String JACKSON_PACKAGE = "jacksonPackage" ;
113113 public static final String ADDITIONAL_NOT_NULL_ANNOTATIONS = "additionalNotNullAnnotations" ;
114+ public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated" ;
115+ public static final String GENERATE_SORT_VALIDATION = "generateSortValidation" ;
116+ public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation" ;
114117
115118 @ Getter
116119 public enum RequestMappingMode {
@@ -186,6 +189,16 @@ public enum RequestMappingMode {
186189 @ Getter @ Setter
187190 protected boolean additionalNotNullAnnotations = false ;
188191 @ Setter boolean useHttpServiceProxyFactoryInterfacesConfigurator = false ;
192+ @ Setter protected boolean autoXSpringPaginated = false ;
193+ @ Setter protected boolean generateSortValidation = false ;
194+ @ Setter protected boolean generatePageableConstraintValidation = false ;
195+
196+ // Map from operationId to allowed sort values for @ValidSort annotation generation
197+ private Map <String , List <String >> sortValidationEnums = new HashMap <>();
198+ // Map from operationId to pageable defaults for @PageableDefault/@SortDefault annotation generation
199+ private Map <String , SpringPageableScanUtils .PageableDefaultsData > pageableDefaultsRegistry = new HashMap <>();
200+ // Map from operationId to pageable constraints for @ValidPageable annotation generation
201+ private Map <String , SpringPageableScanUtils .PageableConstraintsData > pageableConstraintsRegistry = new HashMap <>();
189202
190203 public SpringCodegen () {
191204 super ();
@@ -338,6 +351,21 @@ public SpringCodegen() {
338351 cliOptions .add (CliOption .newBoolean (ADDITIONAL_NOT_NULL_ANNOTATIONS ,
339352 "Add @NotNull to path variables (required by default) and requestBody." ,
340353 additionalNotNullAnnotations ));
354+ cliOptions .add (CliOption .newBoolean (AUTO_X_SPRING_PAGINATED ,
355+ "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. "
356+ + "When enabled, operations with all three parameters will have Pageable support automatically applied. "
357+ + "Operations with x-spring-paginated explicitly set to false will not be auto-detected. "
358+ + "Only applies when library=spring-boot." ,
359+ autoXSpringPaginated ));
360+ cliOptions .add (CliOption .newBoolean (GENERATE_SORT_VALIDATION ,
361+ "Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to paginated operations "
362+ + "whose 'sort' parameter has enum values. Requires useBeanValidation=true and library=spring-boot." ,
363+ generateSortValidation ));
364+ cliOptions .add (CliOption .newBoolean (GENERATE_PAGEABLE_CONSTRAINT_VALIDATION ,
365+ "Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to "
366+ + "paginated operations whose 'page' or 'size' parameter has a maximum constraint. "
367+ + "Requires useBeanValidation=true and library=spring-boot." ,
368+ generatePageableConstraintValidation ));
341369
342370 }
343371
@@ -547,6 +575,12 @@ public void processOpts() {
547575
548576 convertPropertyToBooleanAndWriteBack (ADDITIONAL_NOT_NULL_ANNOTATIONS , this ::setAdditionalNotNullAnnotations );
549577
578+ if (SPRING_BOOT .equals (library )) {
579+ convertPropertyToBooleanAndWriteBack (AUTO_X_SPRING_PAGINATED , this ::setAutoXSpringPaginated );
580+ convertPropertyToBooleanAndWriteBack (GENERATE_SORT_VALIDATION , this ::setGenerateSortValidation );
581+ convertPropertyToBooleanAndWriteBack (GENERATE_PAGEABLE_CONSTRAINT_VALIDATION , this ::setGeneratePageableConstraintValidation );
582+ }
583+
550584 // override parent one
551585 importMapping .put ("JsonDeserialize" , (useJackson3 ? JACKSON3_PACKAGE : JACKSON2_PACKAGE ) + ".databind.annotation.JsonDeserialize" );
552586
@@ -792,6 +826,33 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
792826 (sourceFolder + File .separator + configPackage ).replace ("." , java .io .File .separator ), "EnumConverterConfiguration.java" ));
793827 }
794828
829+ if (SPRING_BOOT .equals (library ) && generateSortValidation && useBeanValidation ) {
830+ sortValidationEnums = SpringPageableScanUtils .scanSortValidationEnums (openAPI , autoXSpringPaginated );
831+ if (!sortValidationEnums .isEmpty ()) {
832+ importMapping .putIfAbsent ("ValidSort" , configPackage + ".ValidSort" );
833+ supportingFiles .add (new SupportingFile ("validSort.mustache" ,
834+ (sourceFolder + File .separator + configPackage ).replace ("." , java .io .File .separator ), "ValidSort.java" ));
835+ }
836+ }
837+
838+ if (SPRING_BOOT .equals (library )) {
839+ pageableDefaultsRegistry = SpringPageableScanUtils .scanPageableDefaults (openAPI , autoXSpringPaginated );
840+ if (!pageableDefaultsRegistry .isEmpty ()) {
841+ importMapping .putIfAbsent ("PageableDefault" , "org.springframework.data.web.PageableDefault" );
842+ importMapping .putIfAbsent ("SortDefault" , "org.springframework.data.web.SortDefault" );
843+ importMapping .putIfAbsent ("Sort" , "org.springframework.data.domain.Sort" );
844+ }
845+ }
846+
847+ if (SPRING_BOOT .equals (library ) && generatePageableConstraintValidation && useBeanValidation ) {
848+ pageableConstraintsRegistry = SpringPageableScanUtils .scanPageableConstraints (openAPI , autoXSpringPaginated );
849+ if (!pageableConstraintsRegistry .isEmpty ()) {
850+ importMapping .putIfAbsent ("ValidPageable" , configPackage + ".ValidPageable" );
851+ supportingFiles .add (new SupportingFile ("validPageable.mustache" ,
852+ (sourceFolder + File .separator + configPackage ).replace ("." , java .io .File .separator ), "ValidPageable.java" ));
853+ }
854+ }
855+
795856 /*
796857 * TODO the following logic should not need anymore in OAS 3.0 if
797858 * ("/".equals(swagger.getBasePath())) { swagger.setBasePath(""); }
@@ -1114,6 +1175,24 @@ protected boolean isConstructorWithAllArgsAllowed(CodegenModel codegenModel) {
11141175 @ Override
11151176 public CodegenOperation fromOperation (String path , String httpMethod , Operation operation , List <Server > servers ) {
11161177
1178+ // Auto-detect pagination parameters and add x-spring-paginated if autoXSpringPaginated is enabled.
1179+ // Only for spring-boot; respect manual x-spring-paginated: false override.
1180+ if (SPRING_BOOT .equals (library ) && autoXSpringPaginated ) {
1181+ if (operation .getExtensions () == null || !Boolean .FALSE .equals (operation .getExtensions ().get ("x-spring-paginated" ))) {
1182+ if (operation .getParameters () != null ) {
1183+ Set <String > paramNames = operation .getParameters ().stream ()
1184+ .map (io .swagger .v3 .oas .models .parameters .Parameter ::getName )
1185+ .collect (Collectors .toSet ());
1186+ if (paramNames .containsAll (Arrays .asList ("page" , "size" , "sort" ))) {
1187+ if (operation .getExtensions () == null ) {
1188+ operation .setExtensions (new HashMap <>());
1189+ }
1190+ operation .getExtensions ().put ("x-spring-paginated" , Boolean .TRUE );
1191+ }
1192+ }
1193+ }
1194+ }
1195+
11171196 // add Pageable import only if x-spring-paginated explicitly used
11181197 // this allows to use a custom Pageable schema without importing Spring Pageable.
11191198 if (Boolean .TRUE .equals (operation .getExtensions ().get ("x-spring-paginated" ))) {
@@ -1142,6 +1221,56 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
11421221 // #8315 Remove matching Spring Data Web default query params if 'x-spring-paginated' with Pageable is used
11431222 codegenOperation .queryParams .removeIf (param -> defaultPageableQueryParams .contains (param .baseName ));
11441223 codegenOperation .allParams .removeIf (param -> param .isQueryParam && defaultPageableQueryParams .contains (param .baseName ));
1224+
1225+ // Build pageable parameter annotations (@ValidPageable, @ValidSort, @PageableDefault, @SortDefault.SortDefaults)
1226+ List <String > pageableAnnotations = new ArrayList <>();
1227+
1228+ if (generatePageableConstraintValidation && useBeanValidation && pageableConstraintsRegistry .containsKey (codegenOperation .operationId )) {
1229+ SpringPageableScanUtils .PageableConstraintsData constraints = pageableConstraintsRegistry .get (codegenOperation .operationId );
1230+ List <String > attrs = new ArrayList <>();
1231+ if (constraints .maxSize >= 0 ) attrs .add ("maxSize = " + constraints .maxSize );
1232+ if (constraints .maxPage >= 0 ) attrs .add ("maxPage = " + constraints .maxPage );
1233+ pageableAnnotations .add ("@ValidPageable(" + String .join (", " , attrs ) + ")" );
1234+ codegenOperation .imports .add ("ValidPageable" );
1235+ }
1236+
1237+ if (generateSortValidation && useBeanValidation && sortValidationEnums .containsKey (codegenOperation .operationId )) {
1238+ List <String > allowedSortValues = sortValidationEnums .get (codegenOperation .operationId );
1239+ // Java annotation arrays use {} syntax
1240+ String allowedValuesStr = allowedSortValues .stream ()
1241+ .map (v -> "\" " + v .replace ("\\ " , "\\ \\ " ).replace ("\" " , "\\ \" " ) + "\" " )
1242+ .collect (Collectors .joining (", " ));
1243+ pageableAnnotations .add ("@ValidSort(allowedValues = {" + allowedValuesStr + "})" );
1244+ codegenOperation .imports .add ("ValidSort" );
1245+ }
1246+
1247+ if (pageableDefaultsRegistry .containsKey (codegenOperation .operationId )) {
1248+ SpringPageableScanUtils .PageableDefaultsData defaults = pageableDefaultsRegistry .get (codegenOperation .operationId );
1249+ if (defaults .page != null || defaults .size != null ) {
1250+ List <String > attrs = new ArrayList <>();
1251+ if (defaults .page != null ) attrs .add ("page = " + defaults .page );
1252+ if (defaults .size != null ) attrs .add ("size = " + defaults .size );
1253+ pageableAnnotations .add ("@PageableDefault(" + String .join (", " , attrs ) + ")" );
1254+ codegenOperation .imports .add ("PageableDefault" );
1255+ }
1256+ if (!defaults .sortDefaults .isEmpty ()) {
1257+ // Java annotation arrays use @SortDefault(...) with {} for the sort field array
1258+ List <String > sortEntries = defaults .sortDefaults .stream ()
1259+ .map (sf -> "@SortDefault(sort = {\" " + sf .field + "\" }, direction = Sort.Direction." + sf .direction + ")" )
1260+ .collect (Collectors .toList ());
1261+ if (sortEntries .size () == 1 ) {
1262+ pageableAnnotations .add ("@SortDefault.SortDefaults(" + sortEntries .get (0 ) + ")" );
1263+ } else {
1264+ pageableAnnotations .add ("@SortDefault.SortDefaults({" + String .join (", " , sortEntries ) + "})" );
1265+ }
1266+ codegenOperation .imports .add ("SortDefault" );
1267+ codegenOperation .imports .add ("Sort" );
1268+ }
1269+ }
1270+
1271+ if (!pageableAnnotations .isEmpty ()) {
1272+ codegenOperation .vendorExtensions .put ("x-pageable-extra-annotation" , pageableAnnotations );
1273+ }
11451274 }
11461275 if (codegenOperation .vendorExtensions .containsKey ("x-spring-provide-args" ) && !provideArgsClassSet .isEmpty ()) {
11471276 codegenOperation .imports .addAll (provideArgsClassSet );
0 commit comments