How to Implement Configurable Data Masking in Spring Boot with Custom Annotations

This article explains a configurable multi‑strategy data‑masking solution for Spring Boot APIs, covering the design of a custom annotation, serializer, AnnotationIntrospector, ObjectMapper integration, and example usage on response objects.

Programmer DD
Programmer DD
Programmer DD
How to Implement Configurable Data Masking in Spring Boot with Custom Annotations

In the afternoon, a product request required masking sensitive data returned by certain APIs, prompting a configurable multi‑strategy data masking solution.

Approach

1. Define a data‑masking annotation and an interface for masking logic, then annotate fields in response classes with the desired masking strategy.

2. Intercept controller responses, locate annotated fields, and apply masking. Instead of using @ControllerAdvice with reflection (which hurts performance), a custom annotation combined with a field serializer similar to @JsonFormat is used.

Code

1. Custom data‑masking annotation with configurable strategy

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataMasking {
    DataMaskingFunc maskFunc() default DataMaskingFunc.NO_MASK;
}

2. Custom serializer for String fields

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 {
        if (Objects.isNull(operation)) {
            String content = DataMaskingFunc.ALL_MASK.operation().mask((String) value, null);
            gen.writeString(content);
        } else {
            String content = operation.mask((String) value, null);
            gen.writeString(content);
        }
    }
}

3. AnnotationIntrospector to bind the annotation to the serializer

@Slf4j
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;
    }
}

4. Override ObjectMapper configuration

@Configuration(proxyBeanMethods = false)
public class DataMaskConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({Jackson2ObjectMapperBuilder.class})
    static class JacksonObjectMapperConfiguration {
        @Bean
        @Primary
        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;
        }
    }
}

5. Apply the annotation to response objects

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;
}
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.

JavaBackend DevelopmentSpring BootJacksondata masking
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.