Dynamic Content Filtering
👉 Implementing JSON Response Filtering in Spring Boot REST APIs Exclusion using Jackson's @JsonFilter
What is Dynamic Content filtering ?
Dynamic filtering refers to a use case where the properties which need to be removed from response, is decided at runtine by the API.
It’s achived by leveraging the Spring utility MappingJacksonValue API and Jackson’s @JsonFilter annotation. Which allows to create filters at rutime to exclude properties of a java bean from the response.
It is crucial for preventing data over-fetching and/or exposing sensitive fields.
Table of Contents
- What is Dynamic Content filtering ?
- Table of Contents
- Features \& Use Case
- Maven Dependency
- Implementation
- Request Flow
- Alternative Approach
- Best Practices
- Security Concerns
Features & Use Case
| Aspect | Description |
|---|---|
| Purpose | Content Filtering refers to dynamically selecting or removing specific properties from a JSON response object before sending it to the client. This is achieved using Jackson’s @JsonFilter mechanism. |
| Use Case | Ideal for APIs where different endpoints need to return the same Java Bean but with varying subsets of fields (e.g., an /users list endpoint needs name and ID, but a /users/{id} detail endpoint also needs address and roles). |
| Benefits | Security (Prevents exposure of sensitive internal fields), Performance (Reduces payload size, thus reducing network bandwidth and processing time), Flexibility (Allows filtering logic to be determined at runtime per API request). |
| Drawbacks | Code Complexity (Requires extra setup in the Controller using MappingJacksonValue and filter providers, making the controller code less clean), Maintainability (Requires diligent management of filter names and fields across multiple endpoints). |
| Prerequisite | This feature is part of jackson-annotations-${version}.jar, implicitly included via the spring-boot-starter-web dependency. |
Maven Dependency
The dynamic filtering feature is provided by Jackson, which is included by default with the spring-boot-starter-web.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Implementation
We have to annotated the Java DTO/VO Bean with @JsonFilter to link it to a named filter.
// SomeBeanDynamicFilter.java
package com.srvivek.sboot.mservices.bean;
import com.fasterxml.jackson.annotation.JsonFilter;
/** Dynamically exclude properties as per the specified filter set by the API. */
@JsonFilter("dyna-filter-for-somebean")
public class SomeBeanDynamicFilter {
private String field1;
private String field2;
private String field3;
private String field4;
private String field5;
private String field6;
// Assuming public constructor SomeBeanDynamicFilter(String... fields) exists
public SomeBeanDynamicFilter(String field1, String field2, String field3, String field4, String field5, String field6) {
this.field1 = field1;
this.field2 = field2;
this.field3 = field3;
this.field4 = field4;
this.field5 = field5;
this.field6 = field6;
}
// ... Getters and Setters omitted for brevity
}
REST Controller
The controller creates the filter, specifies the included/excluded fields, registers the filter by name, and applies it using MappingJacksonValue.
// DynamicFilteringController.java
package com.srvivek.sboot.mservices.controller;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.srvivek.sboot.mservices.bean.SomeBeanDynamicFilter;
@RestController
public class DynamicFilteringController {
private static final String FILTER_NAME = "dyna-filter-for-somebean";
@GetMapping("/dyna-filtering")
public MappingJacksonValue filtering() {
// 1. Define fields to keep (filterOutAllExcept)
final SimpleBeanPropertyFilter simpleBeanPropertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("field2",
"field4", "field6");
// 2. Register filter with the name defined in the Bean
final SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider()
.addFilter(FILTER_NAME, simpleBeanPropertyFilter);
// 3. Construct response bean
final SomeBeanDynamicFilter someBeanDynamicFilter = new SomeBeanDynamicFilter("Value-1", "Value-2", "Value-3",
"Value-4", "Value-5", "Value-6");
// 4. Apply the filter provider to the response bean
final MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(someBeanDynamicFilter);
mappingJacksonValue.setFilters(simpleFilterProvider);
return mappingJacksonValue; // Returns only field2, field4, field6
}
@GetMapping("/dyna-filtering-list")
public MappingJacksonValue filteringList() {
// 1. Define a different set of fields to keep for this API
SimpleBeanPropertyFilter simpleBeanPropertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("field1",
"field3", "field5", "field6");
// 2. Register filter
FilterProvider simpleFilterProvider = new SimpleFilterProvider().addFilter(FILTER_NAME,
simpleBeanPropertyFilter);
// 3. Construct list of response beans
List<SomeBeanDynamicFilter> SomeBeanDynamicFilterList = Arrays.asList(
new SomeBeanDynamicFilter("Value-1", "Value-2", "Value-3", "Value-4", "Value-5", "Value-6"),
new SomeBeanDynamicFilter("Value-11", "Value-22", "Value-33", "Value-44", "Value-55", "Value-66"),
new SomeBeanDynamicFilter("Value-111", "Value-222", "Value-333", "Value-444", "Value-555",
"Value-666"));
// 4. Apply the filter provider to the list
final MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(SomeBeanDynamicFilterList);
mappingJacksonValue.setFilters(simpleFilterProvider);
return mappingJacksonValue; // Returns only field1, field3, field5, field6
}
}
Project POC:
Refer to the following POC application demonstrating static content filtering: Spring Boot POC App - Dynamic Filtering
Insights
| Concept | Description | Role/Mechanism | Key Takeaway |
|---|---|---|---|
Dynamic Filtering | The filtering logic (which fields to include/exclude) is decided at runtime within the controller method, specific to the current API request. | Overrides default serialization. | Provides per-request control over which properties are exposed, unlike Static Filtering. |
@JsonFilter Annotation | Applied as a marker on the Java Bean to define a unique name (e.g., "dyna-filter-for-somebean") that Jackson will use to look up the correct filter implementation. | Acts as the link between the Bean and the FilterProvider. | The name must match the one registered in the FilterProvider. |
MappingJacksonValue | A Spring utility class used to wrap the response object (the Bean or a List of Beans). | Allows for the attachment of a specific FilterProvider to the wrapped response object. | Essential for enabling dynamic, ad-hoc filtering within a Controller method. |
Request Flow
Below sequence diagram illustrates how the dynamic filtering process works when a request is made to a filtered endpoint.
sequenceDiagram
participant Client
participant Controller
participant JacksonMapper
participant ResponseBean
Client->>Controller: GET /dyna-filtering
Controller->>Controller: 1. Create SimpleBeanPropertyFilter (e.g., keep field2, field4, field6)
Controller->>Controller: 2. Create SimpleFilterProvider & register filter ("dyna-filter-for-somebean")
Controller->>ResponseBean: 3. Instantiate SomeBeanDynamicFilter
Controller->>Controller: 4. Wrap Bean in MappingJacksonValue & set Filters
Controller->>JacksonMapper: Serialize(MappingJacksonValue)
JacksonMapper->>JacksonMapper: Check @JsonFilter on ResponseBean
JacksonMapper->>JacksonMapper: Retrieve filter from SimpleFilterProvider
JacksonMapper->>Client: 5. Return Filtered JSON Response (only field2, field4, field6)
Alternative Approach
An alternative to dynamic Jackson filtering is to use Data Transfer Objects (DTOs).
- DTO Approach: For each required subset of fields, create a separate, dedicated DTO class (e.g.,
UserSummaryDTO,UserDetailsDTO). - Mapping: The Controller maps the full JPA Entity/Service Bean to the appropriate DTO before returning the response.
- Trade-off: This approach offers compile-time safety and cleaner controller code (no
MappingJacksonValuelogic) but involves more boilerplate code (creating and maintaining multiple DTO classes and mapping logic).
Best Practices
- Prioritize Static Filtering:
- For fields that should never be exposed (e.g.,
passwordHash), use Static Filtering (@JsonIgnore,@JsonIgnoreProperties). Dynamic filtering should only be used for fields whose inclusion depends on the specific API call.
- For fields that should never be exposed (e.g.,
- Use Enums/Constants:
- Define the filter names (e.g.,
"dyna-filter-for-somebean") as public static final constants in a utility class or the Bean itself to prevent spelling mistakes.
- Define the filter names (e.g.,
- Create a Utility Method:
- Encapsulate the filter creation and application logic (
SimpleBeanPropertyFilter,SimpleFilterProvider,MappingJacksonValue) into a reusable utility method or a customHttpMessageConverterto keep Controller methods cleaner.
- Encapsulate the filter creation and application logic (
- Documentation:
- Clearly document which fields are returned by each API endpoint, especially when dynamic filtering is in use, for better API consumer understanding.
Consider request-based filtering aka Client Driven Filtering as a better long-term solution. Allowing clients to specify which fields they need (e.g., using a fields query parameter like /api/users?fields=id,name) puts the burden of filtering on the client, which is ideal for high-traffic, generic APIs. This involves:
This makes the API more performant and allows consumers to prevent over-fetching data.
Security Concerns
| Concern | Description | Potential Consequence | Mitigation Strategy |
|---|---|---|---|
Accidental Data Exposure | Forgetting to apply the dynamic filter or applying an incorrect one on a response object. | Unintentional exposure of sensitive fields (e.g., passwords, internal IDs, financial data) to unauthorized clients. | Code reviews and automated integration tests that explicitly check JSON payloads for sensitive fields. |
Filter Name Mismatch | The name specified in the bean’s @JsonFilter annotation doesn’t match the name registered in the Jackson SimpleFilterProvider. | The entire bean will be serialized, completely bypassing the intended filtering logic. | Use constants for filter names instead of magic strings to ensure consistency between the bean and the provider. |
Security by Obscurity | Relying only on response filtering to hide data as the primary security defense. | A determined attacker or a simple code oversight could easily expose the hidden data, as there are no other security layers protecting it. | Response filtering must complement robust authentication and authorization (e.g., Spring Security) and not replace them. |