diff --git a/README.md b/README.md
index e95e90a..77a84ce 100644
--- a/README.md
+++ b/README.md
@@ -282,6 +282,97 @@ Just add the dependency. Swagger UI automatically shows:
Works with JPA, MongoDB, and Predicate modules.
+## Pagination, Sorting and Field Selection
+
+The `page-sort` module provides annotations for pagination, sorting, and field selection.
+
+```xml
+
+ com.turkraft.springfilter
+ page-sort
+ 3.2.2
+
+```
+
+### Basic Usage
+
+```java
+@GetMapping("/cars")
+Page search(@Filter Specification spec, @Page Pageable page) {
+ return repository.findAll(spec, page);
+}
+```
+
+Usage: `?page=0&size=20&sort=-year` (prefix `-` for descending)
+
+### Custom Parameter Names
+
+```java
+@GetMapping("/cars")
+Page search(
+ @Page(pageParameter = "p", sizeParameter = "limit", sortParameter = "order") Pageable page) {
+ return repository.findAll(page);
+}
+```
+
+Now use `?p=0&limit=50&order=-year`
+
+### Sort Parameter
+
+```java
+@GetMapping("/cars")
+List search(@Sort org.springframework.data.domain.Sort sort) {
+ return repository.findAll(sort);
+}
+```
+
+Use `?sort=-year` or `?sort=-year,name`
+
+### Field Selection
+
+```java
+@Fields
+@GetMapping("/cars")
+List search() {
+ return repository.findAll();
+}
+```
+
+Use `?fields=id,brand.name,year` to return only specified fields. Uses Jackson's filtering internally.
+
+```java
+// Include specific fields
+?fields= id,name,email
+
+// Exclude fields
+?fields= *,-password,-ssn
+
+// Nested fields
+?fields= id,brand.name,brand.country
+
+// Wildcards
+?fields= user.*
+```
+
+### Combined Example
+
+```java
+@Fields
+@GetMapping("/cars")
+Page search(
+ @Filter Specification spec,
+ @Page Pageable page) {
+ return repository.findAll(spec, page);
+}
+```
+
+Use all features together:
+```
+/cars?filter=year>2020&page=0&size=20&sort=-year&fields=id,brand.name,year
+```
+
+The `openapi` module automatically generates documentation for these parameters when both dependencies are present.
+
## Frontend Integration
### JavaScript
diff --git a/jpa-example/pom.xml b/jpa-example/pom.xml
index 968f350..37838f1 100644
--- a/jpa-example/pom.xml
+++ b/jpa-example/pom.xml
@@ -28,6 +28,12 @@
${revision}
+
+ com.turkraft.springfilter
+ page-sort
+ ${revision}
+
+
com.turkraft.springfilter
openapi
diff --git a/jpa-example/src/main/java/com/turkraft/springfilter/example/SpringFilterJpaExampleApplication.java b/jpa-example/src/main/java/com/turkraft/springfilter/example/SpringFilterJpaExampleApplication.java
index 2467a1f..46e6083 100644
--- a/jpa-example/src/main/java/com/turkraft/springfilter/example/SpringFilterJpaExampleApplication.java
+++ b/jpa-example/src/main/java/com/turkraft/springfilter/example/SpringFilterJpaExampleApplication.java
@@ -1,7 +1,9 @@
package com.turkraft.springfilter.example;
import com.github.javafaker.Faker;
+import com.turkraft.springfilter.boot.Fields;
import com.turkraft.springfilter.boot.Filter;
+import com.turkraft.springfilter.boot.Page;
import com.turkraft.springfilter.converter.FilterSpecification;
import com.turkraft.springfilter.example.model.Address;
import com.turkraft.springfilter.example.model.Company;
@@ -28,6 +30,7 @@
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -59,7 +62,9 @@ public void run(String... args) {
List industries = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Industry industry = new Industry();
- industry.setName(faker.company().industry());
+ industry.setName(faker
+ .company()
+ .industry());
industries.add(industry);
}
industryRepository.saveAll(industries);
@@ -67,11 +72,23 @@ public void run(String... args) {
List companies = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Company company = new Company();
- company.setName(faker.company().name());
- company.setIndustry(faker.options().nextElement(industries));
+ company.setName(faker
+ .company()
+ .name());
+ company.setIndustry(faker
+ .options()
+ .nextElement(industries));
company.setWebsites(
- Stream.generate(() -> Map.entry(faker.company().buzzword(), faker.company().url()))
- .limit(3).skip(faker.random().nextInt(0, 3))
+ Stream
+ .generate(() -> Map.entry(faker
+ .company()
+ .buzzword(), faker
+ .company()
+ .url()))
+ .limit(3)
+ .skip(faker
+ .random()
+ .nextInt(0, 3))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue)));
companies.add(company);
}
@@ -80,21 +97,48 @@ public void run(String... args) {
List employees = new ArrayList<>();
for (int i = 0; i < 30; i++) {
Employee employee = new Employee();
- employee.setFirstName(faker.name().firstName());
- employee.setLastName(faker.name().lastName());
- employee.setBirthDate(faker.date().birthday());
- employee.setMaritalStatus(faker.options().option(MaritalStatus.class));
- employee.setSalary(faker.random().nextInt(1000, 10000));
- employee.setCompany(faker.options().nextElement(companies));
- employee.setManager(employees.isEmpty() ? null : faker.options().nextElement(employees));
+ employee.setFirstName(faker
+ .name()
+ .firstName());
+ employee.setLastName(faker
+ .name()
+ .lastName());
+ employee.setBirthDate(faker
+ .date()
+ .birthday());
+ employee.setMaritalStatus(faker
+ .options()
+ .option(MaritalStatus.class));
+ employee.setSalary(faker
+ .random()
+ .nextInt(1000, 10000));
+ employee.setCompany(faker
+ .options()
+ .nextElement(companies));
+ employee.setManager(employees.isEmpty() ? null : faker
+ .options()
+ .nextElement(employees));
employee.setChildren(
- Stream.generate(faker.name()::firstName).limit(5).skip(faker.random().nextInt(0, 5))
+ Stream
+ .generate(faker.name()::firstName)
+ .limit(5)
+ .skip(faker
+ .random()
+ .nextInt(0, 5))
.collect(Collectors.toList()));
employee.setAddress(new Address() {{
- setCity(faker.address().city());
- setCountry(faker.address().country());
- setPostalCode(faker.address().zipCode());
- setStreetAndNumber(faker.address().streetAddress());
+ setCity(faker
+ .address()
+ .city());
+ setCountry(faker
+ .address()
+ .country());
+ setPostalCode(faker
+ .address()
+ .zipCode());
+ setStreetAndNumber(faker
+ .address()
+ .streetAddress());
}});
employees.add(employee);
}
@@ -103,8 +147,12 @@ public void run(String... args) {
List payslips = new ArrayList<>();
for (int i = 0; i < 50; i++) {
Payslip payslip = new Payslip();
- payslip.setEmployee(faker.options().nextElement(employees));
- payslip.setDate(faker.date().past(360, TimeUnit.DAYS));
+ payslip.setEmployee(faker
+ .options()
+ .nextElement(employees));
+ payslip.setDate(faker
+ .date()
+ .past(360, TimeUnit.DAYS));
payslips.add(payslip);
}
payslipRepository.saveAll(payslips);
@@ -113,32 +161,49 @@ public void run(String... args) {
@Operation(hidden = true)
@GetMapping("/")
- public void index(HttpServletResponse response) throws IOException {
+ public void index(HttpServletResponse response)
+ throws IOException {
response.sendRedirect("swagger-ui.html");
}
- // With springfilter-openapi module, @Filter parameters are automatically documented!
- // No need for manual @Operation and @Parameter annotations.
- // The documentation, examples, and schema are generated automatically from the entity class.
-
@GetMapping(value = "industry")
- public List getIndustries(@Filter FilterSpecification filter) {
- return industryRepository.findAll(filter);
+ @Fields
+ public List getIndustries(
+ @Filter FilterSpecification filter,
+ @Page Pageable pageable) {
+ return industryRepository
+ .findAll(filter, pageable)
+ .getContent();
}
@GetMapping(value = "company")
- public List getCompanies(@Filter FilterSpecification filter) {
- return companyRepository.findAll(filter);
+ @Fields
+ public List getCompanies(
+ @Filter FilterSpecification filter,
+ @Page Pageable pageable) {
+ return companyRepository
+ .findAll(filter, pageable)
+ .getContent();
}
@GetMapping(value = "employee")
- public List getEmployees(@Filter FilterSpecification filter) {
- return employeeRepository.findAll(filter);
+ @Fields
+ public List getEmployees(
+ @Filter FilterSpecification filter,
+ @Page Pageable pageable) {
+ return employeeRepository
+ .findAll(filter, pageable)
+ .getContent();
}
@GetMapping(value = "payslip")
- public List getPayslips(@Filter FilterSpecification filter) {
- return payslipRepository.findAll(filter);
+ @Fields
+ public List getPayslips(
+ @Filter FilterSpecification filter,
+ @Page Pageable pageable) {
+ return payslipRepository
+ .findAll(filter, pageable)
+ .getContent();
}
}
diff --git a/mongo-example/pom.xml b/mongo-example/pom.xml
index 7ab81e4..b970b04 100644
--- a/mongo-example/pom.xml
+++ b/mongo-example/pom.xml
@@ -30,6 +30,12 @@
${revision}
+
+ com.turkraft.springfilter
+ page-sort
+ ${revision}
+
+
com.turkraft.springfilter
openapi
diff --git a/mongo-example/src/main/java/com/turkraft/springfilter/example/SpringFilterMongoExampleApplication.java b/mongo-example/src/main/java/com/turkraft/springfilter/example/SpringFilterMongoExampleApplication.java
index dc1cc1a..d6ba645 100644
--- a/mongo-example/src/main/java/com/turkraft/springfilter/example/SpringFilterMongoExampleApplication.java
+++ b/mongo-example/src/main/java/com/turkraft/springfilter/example/SpringFilterMongoExampleApplication.java
@@ -1,14 +1,15 @@
package com.turkraft.springfilter.example;
import com.github.javafaker.Faker;
+import com.turkraft.springfilter.boot.Fields;
import com.turkraft.springfilter.boot.Filter;
+import com.turkraft.springfilter.boot.Page;
import com.turkraft.springfilter.example.model.Company;
import com.turkraft.springfilter.example.model.Employee;
import com.turkraft.springfilter.example.model.Employee.MaritalStatus;
import com.turkraft.springfilter.example.model.Industry;
import com.turkraft.springfilter.example.model.Payslip;
import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
@@ -24,8 +25,10 @@
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Query;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -177,27 +180,43 @@ public void index(HttpServletResponse response)
}
@GetMapping(value = "industry")
+ @Fields
public List getIndustries(
- @Parameter(hidden = true) @Filter(entityClass = Industry.class) Document filter) {
- return mongoTemplate.find(new BasicQuery(filter), Industry.class);
+ @Filter(entityClass = Industry.class) Document filter,
+ @Page Pageable pageable) {
+ Query query = new BasicQuery(filter);
+ query.with(pageable);
+ return mongoTemplate.find(query, Industry.class);
}
@GetMapping(value = "company")
+ @Fields
public List getCompanies(
- @Parameter(hidden = true) @Filter(entityClass = Company.class) Document filter) {
- return mongoTemplate.find(new BasicQuery(filter), Company.class);
+ @Filter(entityClass = Company.class) Document filter,
+ @Page Pageable pageable) {
+ Query query = new BasicQuery(filter);
+ query.with(pageable);
+ return mongoTemplate.find(query, Company.class);
}
@GetMapping(value = "employee")
+ @Fields
public List getEmployees(
- @Parameter(hidden = true) @Filter(entityClass = Employee.class) Document filter) {
- return mongoTemplate.find(new BasicQuery(filter), Employee.class);
+ @Filter(entityClass = Employee.class) Document filter,
+ @Page Pageable pageable) {
+ Query query = new BasicQuery(filter);
+ query.with(pageable);
+ return mongoTemplate.find(query, Employee.class);
}
@GetMapping(value = "payslip")
+ @Fields
public List getPayslips(
- @Parameter(hidden = true) @Filter(entityClass = Payslip.class) Document filter) {
- return mongoTemplate.find(new BasicQuery(filter), Payslip.class);
+ @Filter(entityClass = Payslip.class) Document filter,
+ @Page Pageable pageable) {
+ Query query = new BasicQuery(filter);
+ query.with(pageable);
+ return mongoTemplate.find(query, Payslip.class);
}
}
diff --git a/openapi/pom.xml b/openapi/pom.xml
index 63b655b..25a6b14 100644
--- a/openapi/pom.xml
+++ b/openapi/pom.xml
@@ -32,6 +32,13 @@
${revision}
+
+ com.turkraft.springfilter
+ page-sort
+ ${revision}
+ true
+
+
org.springframework.boot
spring-boot-autoconfigure
@@ -44,6 +51,12 @@
true
+
+ org.springframework.data
+ spring-data-commons
+ test
+
+
org.springframework.boot
spring-boot-starter-test
diff --git a/openapi/src/main/java/com/turkraft/springfilter/openapi/FilterOpenApiAutoConfiguration.java b/openapi/src/main/java/com/turkraft/springfilter/openapi/FilterOpenApiAutoConfiguration.java
index 36205ad..71bf97f 100644
--- a/openapi/src/main/java/com/turkraft/springfilter/openapi/FilterOpenApiAutoConfiguration.java
+++ b/openapi/src/main/java/com/turkraft/springfilter/openapi/FilterOpenApiAutoConfiguration.java
@@ -7,6 +7,7 @@
import com.turkraft.springfilter.openapi.generator.FilterSchemaGenerator;
import com.turkraft.springfilter.openapi.introspection.EntityIntrospector;
import com.turkraft.springfilter.openapi.springdoc.FilterParameterCustomizer;
+import com.turkraft.springfilter.openapi.springdoc.PageSortParameterCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -67,4 +68,16 @@ public FilterParameterCustomizer filterParameterCustomizer(
return new FilterParameterCustomizer(filterSchemaGenerator, filterExampleGenerator);
}
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnClass(name = {
+ "com.turkraft.springfilter.boot.Sort",
+ "com.turkraft.springfilter.boot.Page",
+ "com.turkraft.springfilter.boot.Fields"
+ })
+ public PageSortParameterCustomizer pageSortParameterCustomizer(
+ EntityIntrospector entityIntrospector) {
+ return new PageSortParameterCustomizer(entityIntrospector);
+ }
+
}
diff --git a/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/BaseParameterCustomizer.java b/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/BaseParameterCustomizer.java
new file mode 100644
index 0000000..ef76415
--- /dev/null
+++ b/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/BaseParameterCustomizer.java
@@ -0,0 +1,133 @@
+package com.turkraft.springfilter.openapi.springdoc;
+
+import com.turkraft.springfilter.boot.Filter;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import java.lang.reflect.Method;
+import org.springframework.lang.Nullable;
+
+public abstract class BaseParameterCustomizer {
+
+ protected void removeAutoDetectedParameter(Operation operation,
+ java.lang.reflect.Parameter parameter) {
+
+ if (operation.getParameters() == null) {
+ return;
+ }
+
+ String paramName = parameter.getName();
+ if (paramName == null) {
+ return;
+ }
+
+ operation
+ .getParameters()
+ .removeIf(param ->
+ paramName.equals(param.getName()) ||
+ (param.getName() != null && param
+ .getName()
+ .startsWith("arg"))
+ );
+
+ }
+
+ protected Parameter findOrCreateParameter(Operation operation, String parameterName) {
+
+ if (operation.getParameters() != null) {
+ for (Parameter param : operation.getParameters()) {
+ if (param != null && parameterName.equals(param.getName())) {
+ return param;
+ }
+ }
+ }
+
+ Parameter parameter = new Parameter();
+ parameter.setName(parameterName);
+ return parameter;
+
+ }
+
+ protected void ensureParameterInOperation(Operation operation, Parameter parameter) {
+
+ if (operation.getParameters() == null) {
+ operation.setParameters(new java.util.ArrayList<>());
+ }
+
+ String parameterName = parameter.getName();
+ if (parameterName == null) {
+ return;
+ }
+
+ boolean exists = false;
+ for (int i = 0; i < operation
+ .getParameters()
+ .size(); i++) {
+ Parameter existingParam = operation
+ .getParameters()
+ .get(i);
+ if (existingParam != null && parameterName.equals(existingParam.getName())) {
+ operation
+ .getParameters()
+ .set(i, parameter);
+ exists = true;
+ break;
+ }
+ }
+
+ if (!exists) {
+ operation
+ .getParameters()
+ .add(parameter);
+ }
+
+ }
+
+ @Nullable
+ protected Class> findEntityClassFromMethod(Method method) {
+
+ if (method == null) {
+ return null;
+ }
+
+ java.lang.reflect.Parameter[] parameters = method.getParameters();
+ if (parameters == null) {
+ return null;
+ }
+
+ for (java.lang.reflect.Parameter parameter : parameters) {
+ Filter filterAnnotation = parameter.getAnnotation(Filter.class);
+
+ if (filterAnnotation != null) {
+
+ Class> entityClass = filterAnnotation.entityClass();
+ if (entityClass != null && entityClass != Void.class) {
+ return entityClass;
+ }
+
+ java.lang.reflect.Type parameterType = parameter.getParameterizedType();
+
+ if (parameterType instanceof java.lang.reflect.ParameterizedType parameterizedType) {
+
+ java.lang.reflect.Type[] typeArguments = parameterizedType.getActualTypeArguments();
+
+ if (typeArguments.length > 0) {
+ if (typeArguments[0] instanceof java.lang.reflect.ParameterizedType innerType) {
+ java.lang.reflect.Type[] innerTypeArguments = innerType.getActualTypeArguments();
+ if (innerTypeArguments.length > 0 && innerTypeArguments[0] instanceof Class) {
+ return (Class>) innerTypeArguments[0];
+ }
+ } else if (typeArguments[0] instanceof Class) {
+ return (Class>) typeArguments[0];
+ }
+ }
+ }
+
+ }
+
+ }
+
+ return null;
+
+ }
+
+}
diff --git a/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/FilterParameterCustomizer.java b/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/FilterParameterCustomizer.java
index 5404529..7070fe0 100644
--- a/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/FilterParameterCustomizer.java
+++ b/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/FilterParameterCustomizer.java
@@ -13,7 +13,8 @@
import org.springframework.lang.Nullable;
import org.springframework.web.method.HandlerMethod;
-public class FilterParameterCustomizer implements OperationCustomizer {
+public class FilterParameterCustomizer extends BaseParameterCustomizer
+ implements OperationCustomizer {
private final FilterSchemaGenerator filterSchemaGenerator;
private final FilterExampleGenerator filterExampleGenerator;
@@ -68,28 +69,6 @@ public Operation customize(@Nullable Operation operation, @Nullable HandlerMetho
}
- private void removeAutoDetectedParameter(Operation operation, java.lang.reflect.Parameter parameter) {
-
- if (operation.getParameters() == null) {
- return;
- }
-
- String paramName = parameter.getName();
- if (paramName == null) {
- return;
- }
-
- operation
- .getParameters()
- .removeIf(param ->
- paramName.equals(param.getName()) ||
- (param.getName() != null && param
- .getName()
- .startsWith("arg"))
- );
-
- }
-
private boolean isFilterType(java.lang.reflect.Parameter parameter) {
Class> type = parameter.getType();
@@ -221,55 +200,4 @@ private void customizeGenericFilterParameter(Operation operation, String paramet
}
- private Parameter findOrCreateParameter(Operation operation, String parameterName) {
-
- if (operation.getParameters() != null) {
- for (Parameter param : operation.getParameters()) {
- if (param != null && parameterName.equals(param.getName())) {
- return param;
- }
- }
- }
-
- Parameter parameter = new Parameter();
- parameter.setName(parameterName);
- return parameter;
-
- }
-
- private void ensureParameterInOperation(Operation operation, Parameter parameter) {
-
- if (operation.getParameters() == null) {
- operation.setParameters(new java.util.ArrayList<>());
- }
-
- String parameterName = parameter.getName();
- if (parameterName == null) {
- return;
- }
-
- boolean exists = false;
- for (int i = 0; i < operation
- .getParameters()
- .size(); i++) {
- Parameter existingParam = operation
- .getParameters()
- .get(i);
- if (existingParam != null && parameterName.equals(existingParam.getName())) {
- operation
- .getParameters()
- .set(i, parameter);
- exists = true;
- break;
- }
- }
-
- if (!exists) {
- operation
- .getParameters()
- .add(parameter);
- }
-
- }
-
}
diff --git a/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/PageSortParameterCustomizer.java b/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/PageSortParameterCustomizer.java
new file mode 100644
index 0000000..921c413
--- /dev/null
+++ b/openapi/src/main/java/com/turkraft/springfilter/openapi/springdoc/PageSortParameterCustomizer.java
@@ -0,0 +1,376 @@
+package com.turkraft.springfilter.openapi.springdoc;
+
+import com.turkraft.springfilter.openapi.introspection.EntityIntrospector;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.media.IntegerSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.springdoc.core.customizers.OperationCustomizer;
+import org.springframework.lang.Nullable;
+import org.springframework.web.method.HandlerMethod;
+
+public class PageSortParameterCustomizer extends BaseParameterCustomizer
+ implements OperationCustomizer {
+
+ private final EntityIntrospector entityIntrospector;
+
+ public PageSortParameterCustomizer(EntityIntrospector entityIntrospector) {
+ this.entityIntrospector = entityIntrospector;
+ }
+
+ @Nullable
+ @Override
+ public Operation customize(@Nullable Operation operation, @Nullable HandlerMethod handlerMethod) {
+
+ if (operation == null || handlerMethod == null) {
+ return operation;
+ }
+
+ Method method = handlerMethod.getMethod();
+ if (method == null) {
+ return operation;
+ }
+
+ Class> entityClass = findEntityClassFromMethod(method);
+
+ try {
+ Class> fieldsAnnotationClass = Class.forName("com.turkraft.springfilter.boot.Fields");
+ Object fieldsAnnotation = method.getAnnotation((Class) fieldsAnnotationClass);
+
+ if (fieldsAnnotation != null) {
+ customizeFieldsParameter(operation, fieldsAnnotation, entityClass);
+ }
+ } catch (ClassNotFoundException ignored) {
+ }
+
+ java.lang.reflect.Parameter[] parameters = method.getParameters();
+ if (parameters == null) {
+ return operation;
+ }
+
+ for (java.lang.reflect.Parameter parameter : parameters) {
+
+ try {
+
+ Class> sortAnnotation = Class.forName("com.turkraft.springfilter.boot.Sort");
+ Class> pageAnnotation = Class.forName("com.turkraft.springfilter.boot.Page");
+
+ Object sort = parameter.getAnnotation((Class) sortAnnotation);
+ Object page = parameter.getAnnotation((Class) pageAnnotation);
+
+ if (sort != null || isSortType(parameter)) {
+ removeAutoDetectedParameter(operation, parameter);
+ customizeSortParameter(operation, sort, parameter, entityClass);
+ }
+
+ if (page != null || isPageType(parameter)) {
+ removeAutoDetectedParameter(operation, parameter);
+ customizePageParameter(operation, page, parameter, entityClass);
+ }
+
+ } catch (ClassNotFoundException ignored) {
+ }
+
+ }
+
+ return operation;
+
+ }
+
+ private boolean isSortType(java.lang.reflect.Parameter parameter) {
+ String typeName = parameter
+ .getType()
+ .getName();
+ return typeName.equals("org.springframework.data.domain.Sort");
+ }
+
+ private boolean isPageType(java.lang.reflect.Parameter parameter) {
+ String typeName = parameter
+ .getType()
+ .getName();
+ return typeName.equals("org.springframework.data.domain.Pageable");
+ }
+
+
+ private void customizeSortParameter(Operation operation, @Nullable Object sortAnnotation,
+ java.lang.reflect.Parameter parameter, @Nullable Class> entityClass) {
+
+ try {
+
+ String parameterName = "sort";
+ boolean required = false;
+ String defaultValue = "";
+ int maxFields = 10;
+
+ if (sortAnnotation != null) {
+ Class> sortClass = sortAnnotation.getClass();
+ parameterName = (String) sortClass
+ .getMethod("parameter")
+ .invoke(sortAnnotation);
+ required = (Boolean) sortClass
+ .getMethod("required")
+ .invoke(sortAnnotation);
+ defaultValue = (String) sortClass
+ .getMethod("defaultValue")
+ .invoke(sortAnnotation);
+ maxFields = (Integer) sortClass
+ .getMethod("maxFields")
+ .invoke(sortAnnotation);
+ }
+
+ Parameter openApiParameter = findOrCreateParameter(operation, parameterName);
+ openApiParameter.setIn("query");
+ openApiParameter.setRequired(required);
+
+ String description = "Sort fields (comma-separated). Use `-` prefix for descending order. "
+ + "Example: `-createdAt,name` sorts by createdAt descending, then name ascending. "
+ + "Max fields: "
+ + maxFields;
+
+ openApiParameter.setDescription(description);
+
+ Schema> schema = new StringSchema();
+ schema.setType("string");
+
+ if (!defaultValue.isEmpty()) {
+ schema.setDefault(defaultValue);
+ }
+
+ String example = generateSortExample(entityClass);
+ schema.setExample(example);
+
+ openApiParameter.setSchema(schema);
+
+ ensureParameterInOperation(operation, openApiParameter);
+
+ } catch (Exception ignored) {
+ }
+
+ }
+
+ private String generateSortExample(@Nullable Class> entityClass) {
+
+ if (entityClass == null) {
+ return "-createdAt,name";
+ }
+
+ try {
+ EntityIntrospector.EntitySchema schema = entityIntrospector.introspect(entityClass);
+ List rootFields = schema
+ .getRootFields()
+ .stream()
+ .filter(f -> !f.isCollection() && !f.isRelation())
+ .map(f -> f.getPath())
+ .limit(2)
+ .collect(Collectors.toList());
+
+ if (rootFields.isEmpty()) {
+ return "-createdAt,name";
+ }
+
+ if (rootFields.size() == 1) {
+ return "-" + rootFields.get(0);
+ }
+
+ return "-" + rootFields.get(0) + "," + rootFields.get(1);
+ } catch (Exception e) {
+ return "-createdAt,name";
+ }
+
+ }
+
+ private void customizePageParameter(Operation operation, @Nullable Object pageAnnotation,
+ java.lang.reflect.Parameter parameter, @Nullable Class> entityClass) {
+
+ try {
+
+ String pageParam = "page";
+ String sizeParam = "size";
+ String sortParam = "sort";
+ int defaultPage = 0;
+ int defaultSize = 20;
+ int maxSize = 100;
+ int sortMaxFields = 10;
+ boolean enableSort = true;
+
+ if (pageAnnotation != null) {
+ Class> pageClass = pageAnnotation.getClass();
+ pageParam = (String) pageClass
+ .getMethod("pageParameter")
+ .invoke(pageAnnotation);
+ sizeParam = (String) pageClass
+ .getMethod("sizeParameter")
+ .invoke(pageAnnotation);
+ sortParam = (String) pageClass
+ .getMethod("sortParameter")
+ .invoke(pageAnnotation);
+ defaultPage = (Integer) pageClass
+ .getMethod("defaultPage")
+ .invoke(pageAnnotation);
+ defaultSize = (Integer) pageClass
+ .getMethod("defaultSize")
+ .invoke(pageAnnotation);
+ maxSize = (Integer) pageClass
+ .getMethod("maxSize")
+ .invoke(pageAnnotation);
+ sortMaxFields = (Integer) pageClass
+ .getMethod("sortMaxFields")
+ .invoke(pageAnnotation);
+ enableSort = (Boolean) pageClass
+ .getMethod("enableSort")
+ .invoke(pageAnnotation);
+ }
+
+ Parameter pageParameter = findOrCreateParameter(operation, pageParam);
+ pageParameter.setIn("query");
+ pageParameter.setRequired(false);
+ pageParameter.setDescription("Page number (zero-based)");
+
+ Schema> pageSchema = new IntegerSchema();
+ pageSchema.setType("integer");
+ pageSchema.setDefault(defaultPage);
+ pageSchema.setMinimum(java.math.BigDecimal.ZERO);
+ pageSchema.setExample(0);
+
+ pageParameter.setSchema(pageSchema);
+ ensureParameterInOperation(operation, pageParameter);
+
+ Parameter sizeParameter = findOrCreateParameter(operation, sizeParam);
+ sizeParameter.setIn("query");
+ sizeParameter.setRequired(false);
+ sizeParameter.setDescription("Page size (max: " + maxSize + ")");
+
+ Schema> sizeSchema = new IntegerSchema();
+ sizeSchema.setType("integer");
+ sizeSchema.setDefault(defaultSize);
+ sizeSchema.setMinimum(java.math.BigDecimal.ONE);
+ sizeSchema.setMaximum(java.math.BigDecimal.valueOf(maxSize));
+ sizeSchema.setExample(defaultSize);
+
+ sizeParameter.setSchema(sizeSchema);
+ ensureParameterInOperation(operation, sizeParameter);
+
+ if (enableSort) {
+ Parameter sortParameter = findOrCreateParameter(operation, sortParam);
+ sortParameter.setIn("query");
+ sortParameter.setRequired(false);
+
+ String description = "Sort fields (comma-separated). Use `-` prefix for descending order. "
+ + "Example: `-createdAt,name` sorts by createdAt descending, then name ascending. "
+ + "Max fields: "
+ + sortMaxFields;
+
+ sortParameter.setDescription(description);
+
+ Schema> sortSchema = new StringSchema();
+ sortSchema.setType("string");
+
+ String example = generateSortExample(entityClass);
+ sortSchema.setExample(example);
+
+ sortParameter.setSchema(sortSchema);
+ ensureParameterInOperation(operation, sortParameter);
+ }
+
+ } catch (Exception ignored) {
+ }
+
+ }
+
+ private void customizeFieldsParameter(Operation operation, @Nullable Object fieldsAnnotation,
+ @Nullable Class> entityClass) {
+
+ try {
+
+ String parameterName = "fields";
+ boolean required = false;
+ String defaultValue = "";
+
+ if (fieldsAnnotation != null) {
+ Class> fieldsClass = fieldsAnnotation.getClass();
+ parameterName = (String) fieldsClass
+ .getMethod("parameter")
+ .invoke(fieldsAnnotation);
+ required = (Boolean) fieldsClass
+ .getMethod("required")
+ .invoke(fieldsAnnotation);
+ defaultValue = (String) fieldsClass
+ .getMethod("defaultValue")
+ .invoke(fieldsAnnotation);
+ }
+
+ Parameter openApiParameter = findOrCreateParameter(operation, parameterName);
+ openApiParameter.setIn("query");
+ openApiParameter.setRequired(required);
+
+ String description = "Fields to include in response (comma-separated). "
+ + "Use `-` or `!` prefix to exclude. Supports wildcards (`*`) and nested paths. "
+ + "Examples: `id,name,email` or `*,-password` or `user.*`";
+
+ openApiParameter.setDescription(description);
+
+ Schema> schema = new StringSchema();
+ schema.setType("string");
+
+ if (!defaultValue.isEmpty()) {
+ schema.setDefault(defaultValue);
+ }
+
+ String example = generateFieldsExample(entityClass);
+ schema.setExample(example);
+
+ openApiParameter.setSchema(schema);
+
+ ensureParameterInOperation(operation, openApiParameter);
+
+ } catch (Exception ignored) {
+ }
+
+ }
+
+ private String generateFieldsExample(@Nullable Class> entityClass) {
+
+ if (entityClass == null) {
+ return "id,name,email";
+ }
+
+ try {
+
+ EntityIntrospector.EntitySchema schema = entityIntrospector.introspect(entityClass);
+ List allFields = schema
+ .getRootFields()
+ .stream()
+ .filter(f -> !f.isCollection() && !f.isRelation())
+ .map(f -> f.getPath())
+ .collect(Collectors.toList());
+
+ if (allFields.isEmpty()) {
+ return "id,name,email";
+ }
+
+ List fieldsForExample = allFields
+ .stream()
+ .skip(2)
+ .limit(3)
+ .collect(Collectors.toList());
+
+ if (fieldsForExample.isEmpty()) {
+ fieldsForExample = allFields
+ .stream()
+ .limit(3)
+ .collect(Collectors.toList());
+ }
+
+ return String.join(",", fieldsForExample);
+
+ } catch (Exception e) {
+ return "id,name,email";
+ }
+
+ }
+
+}
diff --git a/openapi/src/test/java/com/turkraft/springfilter/openapi/PageSortParameterCustomizerTest.java b/openapi/src/test/java/com/turkraft/springfilter/openapi/PageSortParameterCustomizerTest.java
new file mode 100644
index 0000000..3ef1761
--- /dev/null
+++ b/openapi/src/test/java/com/turkraft/springfilter/openapi/PageSortParameterCustomizerTest.java
@@ -0,0 +1,397 @@
+package com.turkraft.springfilter.openapi;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.turkraft.springfilter.boot.Fields;
+import com.turkraft.springfilter.boot.Page;
+import com.turkraft.springfilter.boot.Sort;
+import com.turkraft.springfilter.openapi.introspection.EntityIntrospector;
+import com.turkraft.springfilter.openapi.springdoc.PageSortParameterCustomizer;
+import org.springframework.data.domain.Pageable;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.method.HandlerMethod;
+
+class PageSortParameterCustomizerTest {
+
+ private PageSortParameterCustomizer customizer;
+
+ @BeforeEach
+ void setUp() {
+ EntityIntrospector entityIntrospector = new EntityIntrospector();
+ customizer = new PageSortParameterCustomizer(entityIntrospector);
+ }
+
+ @Test
+ void shouldCustomizeSortParameter() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withSort", org.springframework.data.domain.Sort.class);
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, handlerMethod);
+
+ assertNotNull(result);
+ assertNotNull(result.getParameters());
+ assertEquals(1, result
+ .getParameters()
+ .size());
+
+ Parameter param = result
+ .getParameters()
+ .get(0);
+ assertEquals("sort", param.getName());
+ assertEquals("query", param.getIn());
+ assertFalse(param.getRequired());
+ assertTrue(param
+ .getDescription()
+ .contains("prefix for descending"));
+ assertTrue(param
+ .getDescription()
+ .contains("Example"));
+ }
+
+ @Test
+ void shouldCustomizeSortParameterWithAnnotation() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withSortAnnotation", org.springframework.data.domain.Sort.class);
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, handlerMethod);
+
+ assertNotNull(result);
+ assertNotNull(result.getParameters());
+
+ Optional sortParam = result
+ .getParameters()
+ .stream()
+ .filter(p -> "customSort".equals(p.getName()))
+ .findFirst();
+
+ assertTrue(sortParam.isPresent());
+ assertEquals("customSort", sortParam
+ .get()
+ .getName());
+ assertTrue(sortParam
+ .get()
+ .getRequired());
+ }
+
+ @Test
+ void shouldCustomizePageParameter() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withPage", Pageable.class);
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, handlerMethod);
+
+ assertNotNull(result);
+ assertNotNull(result.getParameters());
+ assertEquals(3, result
+ .getParameters()
+ .size());
+
+ Optional pageParam = result
+ .getParameters()
+ .stream()
+ .filter(p -> "page".equals(p.getName()))
+ .findFirst();
+
+ Optional sizeParam = result
+ .getParameters()
+ .stream()
+ .filter(p -> "size".equals(p.getName()))
+ .findFirst();
+
+ Optional sortParam = result
+ .getParameters()
+ .stream()
+ .filter(p -> "sort".equals(p.getName()))
+ .findFirst();
+
+ assertTrue(pageParam.isPresent());
+ assertTrue(sizeParam.isPresent());
+ assertTrue(sortParam.isPresent());
+
+ assertEquals("query", pageParam
+ .get()
+ .getIn());
+ assertEquals("integer", pageParam
+ .get()
+ .getSchema()
+ .getType());
+ assertEquals(BigDecimal.ZERO, pageParam
+ .get()
+ .getSchema()
+ .getMinimum());
+
+ assertEquals("query", sizeParam
+ .get()
+ .getIn());
+ assertEquals("integer", sizeParam
+ .get()
+ .getSchema()
+ .getType());
+ assertTrue(sizeParam
+ .get()
+ .getDescription()
+ .contains("max"));
+
+ assertEquals("query", sortParam
+ .get()
+ .getIn());
+ assertEquals("string", sortParam
+ .get()
+ .getSchema()
+ .getType());
+ assertTrue(sortParam
+ .get()
+ .getDescription()
+ .contains("prefix for descending"));
+ }
+
+ @Test
+ void shouldCustomizePageParameterWithAnnotation() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withPageAnnotation", Pageable.class);
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, handlerMethod);
+
+ assertNotNull(result);
+ assertNotNull(result.getParameters());
+
+ Optional pageParam = result
+ .getParameters()
+ .stream()
+ .filter(p -> "p".equals(p.getName()))
+ .findFirst();
+
+ Optional sizeParam = result
+ .getParameters()
+ .stream()
+ .filter(p -> "s".equals(p.getName()))
+ .findFirst();
+
+ Optional sortParam = result
+ .getParameters()
+ .stream()
+ .filter(p -> "order".equals(p.getName()))
+ .findFirst();
+
+ assertTrue(pageParam.isPresent());
+ assertTrue(sizeParam.isPresent());
+ assertTrue(sortParam.isPresent());
+
+ assertEquals(1, pageParam
+ .get()
+ .getSchema()
+ .getDefault());
+ assertEquals(50, sizeParam
+ .get()
+ .getSchema()
+ .getDefault());
+ assertEquals(BigDecimal.valueOf(200), sizeParam
+ .get()
+ .getSchema()
+ .getMaximum());
+ }
+
+ @Test
+ void shouldCustomizeFieldsParameter() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withFields");
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, handlerMethod);
+
+ assertNotNull(result);
+ assertNotNull(result.getParameters());
+ assertEquals(1, result
+ .getParameters()
+ .size());
+
+ Parameter param = result
+ .getParameters()
+ .get(0);
+ assertEquals("fields", param.getName());
+ assertEquals("query", param.getIn());
+ assertFalse(param.getRequired());
+ assertTrue(param
+ .getDescription()
+ .contains("include in response"));
+ assertTrue(param
+ .getDescription()
+ .contains("wildcard"));
+ }
+
+ @Test
+ void shouldCustomizeFieldsParameterWithAnnotation() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withFieldsAnnotation");
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, handlerMethod);
+
+ assertNotNull(result);
+ assertNotNull(result.getParameters());
+
+ Optional fieldsParam = result
+ .getParameters()
+ .stream()
+ .filter(p -> "select".equals(p.getName()))
+ .findFirst();
+
+ assertTrue(fieldsParam.isPresent());
+ assertEquals("select", fieldsParam
+ .get()
+ .getName());
+ assertTrue(fieldsParam
+ .get()
+ .getRequired());
+ assertEquals("id,name", fieldsParam
+ .get()
+ .getSchema()
+ .getDefault());
+ }
+
+ @Test
+ void shouldCustomizeAllParametersTogether() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withAllAnnotations",
+ org.springframework.data.domain.Sort.class, Pageable.class);
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, handlerMethod);
+
+ assertNotNull(result);
+ assertNotNull(result.getParameters());
+ assertEquals(4, result
+ .getParameters()
+ .size());
+
+ assertTrue(result
+ .getParameters()
+ .stream()
+ .anyMatch(p -> "sort".equals(p.getName())));
+ assertTrue(result
+ .getParameters()
+ .stream()
+ .anyMatch(p -> "page".equals(p.getName())));
+ assertTrue(result
+ .getParameters()
+ .stream()
+ .anyMatch(p -> "size".equals(p.getName())));
+ assertTrue(result
+ .getParameters()
+ .stream()
+ .anyMatch(p -> "fields".equals(p.getName())));
+ }
+
+ @Test
+ void shouldNotIncludeSortParameterWhenDisabled() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withPageNoSort", Pageable.class);
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, handlerMethod);
+
+ assertNotNull(result);
+ assertNotNull(result.getParameters());
+ assertEquals(2, result.getParameters().size());
+
+ assertTrue(result.getParameters().stream().anyMatch(p -> "page".equals(p.getName())));
+ assertTrue(result.getParameters().stream().anyMatch(p -> "size".equals(p.getName())));
+ assertFalse(result.getParameters().stream().anyMatch(p -> "sort".equals(p.getName())));
+ }
+
+ @Test
+ void shouldHandleNullOperation() throws Exception {
+ TestController controller = new TestController();
+ Method method = TestController.class.getMethod("withSort", org.springframework.data.domain.Sort.class);
+ HandlerMethod handlerMethod = new HandlerMethod(controller, method);
+
+ Operation result = customizer.customize(null, handlerMethod);
+
+ assertEquals(null, result);
+ }
+
+ @Test
+ void shouldHandleNullHandlerMethod() {
+ Operation operation = new Operation();
+ Operation result = customizer.customize(operation, null);
+
+ assertEquals(operation, result);
+ }
+
+ @RestController
+ static class TestController {
+
+ @GetMapping("/sort")
+ public String withSort(@Sort org.springframework.data.domain.Sort sort) {
+ return "ok";
+ }
+
+ @GetMapping("/sort-annotation")
+ public String withSortAnnotation(@Sort(parameter = "customSort", required = true) org.springframework.data.domain.Sort sort) {
+ return "ok";
+ }
+
+ @GetMapping("/page")
+ public String withPage(@Page Pageable page) {
+ return "ok";
+ }
+
+ @GetMapping("/page-annotation")
+ public String withPageAnnotation(
+ @Page(pageParameter = "p", sizeParameter = "s", sortParameter = "order", defaultPage = 1, defaultSize = 50, maxSize = 200) Pageable page) {
+ return "ok";
+ }
+
+ @GetMapping("/page-no-sort")
+ public String withPageNoSort(@Page(enableSort = false) Pageable page) {
+ return "ok";
+ }
+
+ @Fields
+ @GetMapping("/fields")
+ public String withFields() {
+ return "ok";
+ }
+
+ @Fields(parameter = "select", required = true, defaultValue = "id,name")
+ @GetMapping("/fields-annotation")
+ public String withFieldsAnnotation() {
+ return "ok";
+ }
+
+ @Fields
+ @GetMapping("/all")
+ public String withAllAnnotations(
+ @Sort org.springframework.data.domain.Sort sort,
+ @Page Pageable page) {
+ return "ok";
+ }
+
+ }
+
+}
diff --git a/page-sort/pom.xml b/page-sort/pom.xml
new file mode 100644
index 0000000..133e3e7
--- /dev/null
+++ b/page-sort/pom.xml
@@ -0,0 +1,74 @@
+
+
+
+ 4.0.0
+
+
+ com.turkraft.springfilter
+ parent
+ ${revision}
+
+
+ page-sort
+
+ ${project.groupId}:${project.artifactId}
+
+
+
+
+ com.turkraft.springfilter
+ core
+ ${revision}
+ compile
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+
+ org.springframework.data
+ spring-data-commons
+ provided
+
+
+
+ org.springframework
+ spring-webmvc
+ provided
+ true
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ provided
+ true
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ provided
+ true
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
diff --git a/page-sort/src/main/java/com/turkraft/springfilter/boot/Fields.java b/page-sort/src/main/java/com/turkraft/springfilter/boot/Fields.java
new file mode 100644
index 0000000..e707864
--- /dev/null
+++ b/page-sort/src/main/java/com/turkraft/springfilter/boot/Fields.java
@@ -0,0 +1,20 @@
+package com.turkraft.springfilter.boot;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Fields {
+
+ String DEFAULT_PARAMETER_NAME = "fields";
+
+ String parameter() default DEFAULT_PARAMETER_NAME;
+
+ boolean required() default false;
+
+ String defaultValue() default "";
+
+}
diff --git a/page-sort/src/main/java/com/turkraft/springfilter/boot/FieldsFilterAdvice.java b/page-sort/src/main/java/com/turkraft/springfilter/boot/FieldsFilterAdvice.java
new file mode 100644
index 0000000..6bdeea7
--- /dev/null
+++ b/page-sort/src/main/java/com/turkraft/springfilter/boot/FieldsFilterAdvice.java
@@ -0,0 +1,122 @@
+package com.turkraft.springfilter.boot;
+
+import com.fasterxml.jackson.databind.ser.FilterProvider;
+import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
+import com.turkraft.springfilter.pagesort.AntPathFilterMixin;
+import com.turkraft.springfilter.pagesort.AntPathPropertyFilter;
+import com.turkraft.springfilter.pagesort.FieldsExpression;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJacksonValue;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+@RestControllerAdvice
+@ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper")
+public class FieldsFilterAdvice implements ResponseBodyAdvice