Configurable Data Masking in Java Backend Using Custom Annotations and Jackson
This article explains how to implement a flexible, annotation‑driven data‑masking solution for Java backend services by defining custom masking annotations, serializers, and integrating them into Spring's ObjectMapper to automatically mask sensitive fields in API responses.
The author describes a real‑world requirement where certain API responses contain sensitive data that must be masked before being sent to the client.
To avoid repetitive masking logic, a configurable, multi‑strategy approach is proposed: define a custom annotation @DataMasking that can be placed on fields or classes, and specify a masking strategy via an enum.
During serialization, the controller's return objects are intercepted; fields annotated with @DataMasking are processed by a custom Jackson serializer that applies the chosen masking function.
Implementation details:
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataMasking {
DataMaskingFunc maskFunc() default DataMaskingFunc.NO_MASK;
} public interface DataMaskingOperation {
String MASK_CHAR = "*";
String mask(String content, String maskChar);
}
public enum DataMaskingFunc {
NO_MASK((str, maskChar) -> { return str; }),
ALL_MASK((str, maskChar) -> {
if (StringUtils.hasLength(str)) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
sb.append(StringUtils.hasLength(maskChar) ? maskChar : DataMaskingOperation.MASK_CHAR);
}
return sb.toString();
} else {
return str;
}
});
private final DataMaskingOperation operation;
private DataMaskingFunc(DataMaskingOperation operation) { this.operation = operation; }
public DataMaskingOperation operation() { return this.operation; }
} public final class DataMaskingSerializer extends StdScalarSerializer<Object> {
private final DataMaskingOperation operation;
public DataMaskingSerializer() { super(String.class, false); this.operation = null; }
public DataMaskingSerializer(DataMaskingOperation operation) { super(String.class, false); this.operation = operation; }
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
String content = (operation == null)
? DataMaskingFunc.ALL_MASK.operation().mask((String) value, null)
: operation.mask((String) value, null);
gen.writeString(content);
}
// other required overrides omitted for brevity
} public class DataMaskingAnnotationIntrospector extends NopAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
DataMasking annotation = am.getAnnotation(DataMasking.class);
if (annotation != null) {
return new DataMaskingSerializer(annotation.maskFunc().operation());
}
return null;
}
} @Configuration(proxyBeanMethods = false)
static class JacksonObjectMapperConfiguration {
@Bean
@Primary
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector ai = objectMapper.getSerializationConfig().getAnnotationIntrospector();
AnnotationIntrospector newAi = AnnotationIntrospectorPair.pair(ai, new DataMaskingAnnotationIntrospector());
objectMapper.setAnnotationIntrospector(newAi);
return objectMapper;
}
} public class User implements Serializable {
private Long id;
@DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
private String name;
private Integer age;
@DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
private String email;
}By registering the custom AnnotationIntrospector with Spring's primary ObjectMapper, any response object containing the @DataMasking annotation will automatically have its annotated fields masked according to the selected strategy, eliminating repetitive manual masking code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
