Dynamic Reponse Body Attribute Filtering
Response body attribute filtering is a nice feature to have when creating REST APIs. You enable the client to choose which attributes should be returned in the HTTP response.
Let's say you have an endpoint /users
that returns a list of users when a GET request is sent:
[
{
"id": 1,
"firstName": "John",
"lastName": "Doe"
},
{
"id": 2,
"firstName": "Jane",
"lastName": "Doe"
}
]
Then you want to filter the response attributes using a query string like: /users?fields=id,firstName
that will produce:
[
{
"id": 1,
"firstName": "John"
},
{
"id": 2,
"firstName": "Jane"
}
]
Luckily, dynamic response body attribute filtering is really easy to achieve and requires a minimal setup when using Spring framework.
We can make use of JsonFilter and AbstractMappingJacksonResponseBodyAdvice to implement a dynamic response body filter.
So let's implement a filter composed of:
Filtering Out
: Includes a list of fields. E.g.:/users?fields=id,FirstName
Exclusion
: Excludes a list of fields. E.g.:/users?exclude=id,lastName
You can find the source code of a demo here.
Dependencies
The required dependencies to add to the project's pom.xml file:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Implementation
Let's start by creating an annotation that will enable response body attribute filtering whenever is placed on a request handler method:
import static com.example.demo.ResponseBodyFilterAdvice.*;
@Documented
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBodyFilter {
String filterOut() default "fields";
String exclude() default "exclude";
String delimiter() default "\\s*,\\s*";
String filter() default DEFAULT_RESPONSE_BODY_FILTER;
}
We also need a controller advice that will apply the filtering:
@ControllerAdvice
public class ResponseBodyFilterAdvice extends AbstractMappingJacksonResponseBodyAdvice {
public static final String DEFAULT_RESPONSE_BODY_FILTER = "RESPONSE_BODY_FILTER";
private final SimpleFilterProvider filterProvider = new SimpleFilterProvider();
private final String[] EMPTY_FIELDS = new String[]{};
@Override
protected void beforeBodyWriteInternal(MappingJacksonValue container,
MediaType mediaType,
MethodParameter methodParameter,
ServerHttpRequest request,
ServerHttpResponse response) {
FilterProvider filters = container.getFilters();
ResponseBodyFilter responseBodyFilter = methodParameter.getMethodAnnotation(ResponseBodyFilter.class);
if (Objects.isNull(responseBodyFilter) || Objects.nonNull(filters)) {
return;
}
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
String filterOutParameter = responseBodyFilter.filterOut();
String excludeParameter = responseBodyFilter.exclude();
String delimiter = responseBodyFilter.delimiter();
String[] filterOutFields = parseFilterAttribute(servletRequest, filterOutParameter, delimiter);
String[] excludeFields = parseFilterAttribute(servletRequest, excludeParameter, delimiter);
PropertyFilter filter = getPropertyFilter(filterOutFields, excludeFields);
filterProvider.addFilter(responseBodyFilter.filter(), filter);
container.setFilters(filterProvider);
}
private PropertyFilter getPropertyFilter(String[] filterOut, String[] exclude) {
if (exclude.length > 0) {
return SimpleBeanPropertyFilter.serializeAllExcept(exclude);
}
if (filterOut.length > 0) {
return SimpleBeanPropertyFilter.filterOutAllExcept(filterOut);
}
return SimpleBeanPropertyFilter.serializeAll();
}
private String[] parseFilterAttribute(HttpServletRequest servletRequest, String parameter, String delimiter) {
String value = servletRequest.getParameter(parameter);
if (Objects.isNull(value)) {
return EMPTY_FIELDS;
}
String[] attributes = value.split(delimiter);
String[] fields = Arrays.stream(attributes).filter(e -> !e.isEmpty()).toArray(String[]::new);
return fields.length == 0
? EMPTY_FIELDS
: fields;
}
}
The required setup to filter response attributes is done. Let's create a controller and a response entity to test the filter:
import static com.example.demo.ResponseBodyFilterAdvice.*;
@JsonFilter(DEFAULT_RESPONSE_BODY_FILTER)
public class UserResponse {
private String id;
private String firstName;
private String lastName;
// getters and setters
}
@RestController
@RequestMapping("/users")
public class UserController {
private final List<UserResponse> users = createUsers();
@GetMapping
@ResponseBodyFilter
public List<UserResponse> getAllUsers() {
return users;
}
private List<UserResponse> createUsers() {
UserResponse john = new UserResponse();
john.setId(UUID.randomUUID().toString());
john.setFirstName("John");
john.setLastName("Doe");
UserResponse jane = new UserResponse();
jane.setId(UUID.randomUUID().toString());
jane.setFirstName("Jane");
jane.setLastName("Doakes");
return Arrays.asList(john, jane);
}
}
Finally, we can create the main class to run the project:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Once the application is running we can send a GET request to the endpoint /users
, the following response is expected:
[
{
"id": "709a6c69-b025-4007-be1a-73eb2c7de6fe",
"firstName": "John",
"lastName": "Doe"
},
{
"id": "22fb3b32-9bcb-498b-b750-4b170aad56a9",
"firstName": "Jane",
"lastName": "Doakes"
}
]
We can then filter the attributes by sending a query string with the attributes fields
or exclude
:
http://localhost:8080/users?exclude=id
[
{
"firstName": "John",
"lastName": "Doe"
},
{
"firstName": "Jane",
"lastName": "Doakes"
}
]
http://localhost:8080/users?fields=id,firstName
[
{
"id": "709a6c69-b025-4007-be1a-73eb2c7de6fe",
"firstName": "John"
},
{
"id": "22fb3b32-9bcb-498b-b750-4b170aad56a9",
"firstName": "Jane"
}
]
You can also replace the request attributes used to filter the response:
@ResponseBodyFilter(
filterOut = "include",
exclude = "remove",
delimiter = "\\|",
filter = "someOtherFilterName"
)
The example above will look for the parameters include
and remove
to filter attributes separated by |
(%7C
):
/users?include=lastName%7CfirstName
/users?remove=id