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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Configurable Data Masking in Java Backend Using Custom Annotations and Jackson

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

springannotationJacksondata masking
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.