2222import com .samskivert .mustache .Template ;
2323import io .swagger .v3 .oas .models .OpenAPI ;
2424import io .swagger .v3 .oas .models .Operation ;
25+ import io .swagger .v3 .oas .models .PathItem ;
26+ import io .swagger .v3 .oas .models .media .Schema ;
27+ import io .swagger .v3 .oas .models .parameters .Parameter ;
2528import lombok .Getter ;
2629import lombok .Setter ;
2730import org .openapitools .codegen .*;
@@ -98,6 +101,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
98101 public static final String USE_REQUEST_MAPPING_ON_CONTROLLER = "useRequestMappingOnController" ;
99102 public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface" ;
100103 public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated" ;
104+ public static final String GENERATE_SORT_VALIDATION = "generateSortValidation" ;
101105 public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces" ;
102106 public static final String COMPANION_OBJECT = "companionObject" ;
103107
@@ -164,6 +168,7 @@ public String getDescription() {
164168 @ Setter private DeclarativeInterfaceReactiveMode declarativeInterfaceReactiveMode = DeclarativeInterfaceReactiveMode .coroutines ;
165169 @ Setter private boolean useResponseEntity = true ;
166170 @ Setter private boolean autoXSpringPaginated = false ;
171+ @ Setter private boolean generateSortValidation = false ;
167172 @ Setter private boolean useSealedResponseInterfaces = false ;
168173 @ Setter private boolean companionObject = false ;
169174
@@ -180,6 +185,9 @@ public String getDescription() {
180185 private Map <String , String > sealedInterfaceToOperationId = new HashMap <>();
181186 private boolean sealedInterfacesFileWritten = false ;
182187
188+ // Map from operationId to allowed sort values for @ValidSort annotation generation
189+ private Map <String , List <String >> sortValidationEnums = new HashMap <>();
190+
183191 public KotlinSpringServerCodegen () {
184192 super ();
185193
@@ -272,6 +280,7 @@ public KotlinSpringServerCodegen() {
272280 addOption (SCHEMA_IMPLEMENTS , "A map of single interface or a list of interfaces per schema name that should be implemented (serves similar purpose as `x-kotlin-implements`, but is fully decoupled from the api spec). Example: yaml `schemaImplements: {Pet: com.some.pack.WithId, Category: [com.some.pack.CategoryInterface], Dog: [com.some.pack.Canine, com.some.pack.OtherInterface]}` implements interfaces in schemas `Pet` (interface `com.some.pack.WithId`), `Category` (interface `com.some.pack.CategoryInterface`), `Dog`(interfaces `com.some.pack.Canine`, `com.some.pack.OtherInterface`)" , "empty map" );
273281 addOption (SCHEMA_IMPLEMENTS_FIELDS , "A map of single field or a list of fields per schema name that should be prepended with `override` (serves similar purpose as `x-kotlin-implements-fields`, but is fully decoupled from the api spec). Example: yaml `schemaImplementsFields: {Pet: id, Category: [name, id], Dog: [bark, breed]}` marks fields to be prepended with `override` in schemas `Pet` (field `id`), `Category` (fields `name`, `id`) and `Dog` (fields `bark`, `breed`)" , "empty map" );
274282 addSwitch (AUTO_X_SPRING_PAGINATED , "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected." , autoXSpringPaginated );
283+ addSwitch (GENERATE_SORT_VALIDATION , "Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to paginated operations whose 'sort' parameter has enum values. Requires useBeanValidation=true and library=spring-boot." , generateSortValidation );
275284 addSwitch (COMPANION_OBJECT , "Whether to generate companion objects in data classes, enabling companion extensions." , companionObject );
276285 supportedLibraries .put (SPRING_BOOT , "Spring-boot Server application." );
277286 supportedLibraries .put (SPRING_CLOUD_LIBRARY ,
@@ -704,6 +713,10 @@ public void processOpts() {
704713 this .setAutoXSpringPaginated (convertPropertyToBoolean (AUTO_X_SPRING_PAGINATED ));
705714 }
706715 writePropertyBack (AUTO_X_SPRING_PAGINATED , autoXSpringPaginated );
716+ if (additionalProperties .containsKey (GENERATE_SORT_VALIDATION ) && library .equals (SPRING_BOOT )) {
717+ this .setGenerateSortValidation (convertPropertyToBoolean (GENERATE_SORT_VALIDATION ));
718+ }
719+ writePropertyBack (GENERATE_SORT_VALIDATION , generateSortValidation );
707720 if (isUseSpringBoot3 () && isUseSpringBoot4 ()) {
708721 throw new IllegalArgumentException ("Choose between Spring Boot 3 and Spring Boot 4" );
709722 }
@@ -1042,6 +1055,22 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
10421055 }
10431056
10441057 // #8315 Remove matching Spring Data Web default query params if 'x-spring-paginated' with Pageable is used
1058+ // Before removal, capture sort enum values for @ValidSort if generateSortValidation is enabled
1059+ if (generateSortValidation && useBeanValidation && sortValidationEnums .containsKey (codegenOperation .operationId )) {
1060+ List <String > allowedSortValues = sortValidationEnums .get (codegenOperation .operationId );
1061+ String allowedValuesStr = allowedSortValues .stream ()
1062+ .map (v -> "\" " + v .replace ("\\ " , "\\ \\ " ).replace ("\" " , "\\ \" " ) + "\" " )
1063+ .collect (Collectors .joining (", " ));
1064+ String validSortAnnotation = "@ValidSort(allowedValues = [" + allowedValuesStr + "])" ;
1065+
1066+ Object existingAnnotation = codegenOperation .vendorExtensions .get ("x-operation-extra-annotation" );
1067+ List <String > existingAnnotations = DefaultCodegen .getObjectAsStringList (existingAnnotation );
1068+ List <String > updatedAnnotations = new ArrayList <>(existingAnnotations );
1069+ updatedAnnotations .add (validSortAnnotation );
1070+ codegenOperation .vendorExtensions .put ("x-operation-extra-annotation" , updatedAnnotations );
1071+
1072+ codegenOperation .imports .add ("ValidSort" );
1073+ }
10451074 codegenOperation .queryParams .removeIf (param -> defaultPageableQueryParams .contains (param .baseName ));
10461075 codegenOperation .allParams .removeIf (param -> param .isQueryParam && defaultPageableQueryParams .contains (param .baseName ));
10471076 }
@@ -1058,6 +1087,10 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
10581087 (sourceFolder + File .separator + configPackage ).replace ("." , java .io .File .separator ), "EnumConverterConfiguration.kt" ));
10591088 }
10601089
1090+ if (SPRING_BOOT .equals (library ) && generateSortValidation && useBeanValidation ) {
1091+ scanSortValidationEnums (openAPI );
1092+ }
1093+
10611094 if (!additionalProperties .containsKey (TITLE )) {
10621095 // The purpose of the title is for:
10631096 // - README documentation
@@ -1123,6 +1156,78 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
11231156 // TODO: Handle tags
11241157 }
11251158
1159+ /**
1160+ * Scans the OpenAPI spec for paginated operations whose 'sort' parameter has enum values,
1161+ * builds the {@link #sortValidationEnums} registry, and registers the ValidSort.kt supporting file.
1162+ * Called from {@link #preprocessOpenAPI} when {@code generateSortValidation} is enabled.
1163+ */
1164+ private void scanSortValidationEnums (OpenAPI openAPI ) {
1165+ if (openAPI .getPaths () == null ) {
1166+ return ;
1167+ }
1168+ boolean foundAny = false ;
1169+ for (Map .Entry <String , PathItem > pathEntry : openAPI .getPaths ().entrySet ()) {
1170+ for (Operation operation : pathEntry .getValue ().readOperations ()) {
1171+ String operationId = operation .getOperationId ();
1172+ if (operationId == null || !willBePageable (operation )) {
1173+ continue ;
1174+ }
1175+ if (operation .getParameters () == null ) {
1176+ continue ;
1177+ }
1178+ for (Parameter param : operation .getParameters ()) {
1179+ if (!"sort" .equals (param .getName ())) {
1180+ continue ;
1181+ }
1182+ Schema <?> schema = param .getSchema ();
1183+ if (schema == null ) {
1184+ continue ;
1185+ }
1186+ if (schema .get$ref () != null ) {
1187+ schema = ModelUtils .getReferencedSchema (openAPI , schema );
1188+ }
1189+ if (schema == null || schema .getEnum () == null || schema .getEnum ().isEmpty ()) {
1190+ continue ;
1191+ }
1192+ List <String > enumValues = schema .getEnum ().stream ()
1193+ .map (Object ::toString )
1194+ .collect (Collectors .toList ());
1195+ sortValidationEnums .put (operationId , enumValues );
1196+ foundAny = true ;
1197+ }
1198+ }
1199+ }
1200+ if (foundAny ) {
1201+ importMapping .putIfAbsent ("ValidSort" , configPackage + ".ValidSort" );
1202+ supportingFiles .add (new SupportingFile ("validSort.mustache" ,
1203+ (sourceFolder + File .separator + configPackage ).replace ("." , File .separator ), "ValidSort.kt" ));
1204+ }
1205+ }
1206+
1207+ /**
1208+ * Returns true if the given operation will have a Pageable parameter injected — either because
1209+ * it has {@code x-spring-paginated: true} explicitly, or because {@link #autoXSpringPaginated}
1210+ * is enabled and the operation has all three default pagination query parameters (page, size, sort).
1211+ */
1212+ private boolean willBePageable (Operation operation ) {
1213+ if (operation .getExtensions () != null ) {
1214+ Object paginated = operation .getExtensions ().get ("x-spring-paginated" );
1215+ if (Boolean .FALSE .equals (paginated )) {
1216+ return false ;
1217+ }
1218+ if (Boolean .TRUE .equals (paginated )) {
1219+ return true ;
1220+ }
1221+ }
1222+ if (autoXSpringPaginated && operation .getParameters () != null ) {
1223+ Set <String > paramNames = operation .getParameters ().stream ()
1224+ .map (Parameter ::getName )
1225+ .collect (Collectors .toSet ());
1226+ return paramNames .containsAll (Arrays .asList ("page" , "size" , "sort" ));
1227+ }
1228+ return false ;
1229+ }
1230+
11261231 @ Override
11271232 public void postProcessModelProperty (CodegenModel model , CodegenProperty property ) {
11281233 super .postProcessModelProperty (model , property );
0 commit comments