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

This article explains how to create a flexible, annotation‑driven data‑masking solution for Spring Boot APIs by defining custom masking strategies, integrating a Jackson serializer, and configuring the ObjectMapper to automatically mask sensitive fields in response objects.

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

During a casual afternoon, a product manager asked for a feature that masks sensitive data returned by certain APIs. To avoid writing repetitive masking code for each endpoint, a configurable, annotation‑based solution is designed.

Data masking diagram
Data masking diagram

Approach

1. Define a custom data‑masking annotation and a strategy interface so that each field can specify which masking operation to use.

2. Intercept controller responses and apply the masking logic. Instead of using @ControllerAdvice with heavy reflection, a custom Jackson serializer (similar to @JsonFormat) is created to handle the masking efficiently.

Code

1. Custom annotation with configurable masking strategy

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

2. Custom serializer (StringSerializer) for masking

public interface DataMaskingOperation {
    String MASK_CHAR = "*";
    String mask(String content, String maskChar);
}

public enum DataMaskingFunc {
    NO_MASK((str, maskChar) -> 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);
    }
    @Override
    public JsonNode getSchema(SerializerProvider provider, Type typeHint) { return createSchemaNode("string", true); }
    @Override
    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
        visitStringFormat(visitor, typeHint);
    }
}

3. Custom AnnotationIntrospector to bind annotation to 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 to register the introspector

@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 {
    /** 主键ID */
    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.

JavaSpring BootannotationJacksondata masking
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.