Configurable Data Masking in Spring Boot Using Custom Annotations and Jackson

This article demonstrates how to implement a flexible data‑masking solution for sensitive fields in Spring Boot APIs by defining a custom annotation, creating a serializer, integrating it with Jackson via an AnnotationIntrospector, configuring the ObjectMapper, and applying the annotation to domain objects.

Top Architect
Top Architect
Top Architect
Configurable Data Masking in Spring Boot Using Custom Annotations and Jackson

During a product discussion the author was asked to mask sensitive data returned by certain API endpoints. To avoid writing repetitive masking logic for each interface, a configurable, annotation‑driven solution is proposed.

Approach

The solution consists of five steps: define a data‑masking annotation, implement a custom serializer, create an AnnotationIntrospector that links the annotation to the serializer, override the default ObjectMapper to register the introspector, and finally annotate the fields that need masking.

1. Custom DataMasking Annotation

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

2. Custom Serializer (based on Jackson's StdScalarSerializer )

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 boolean isEmpty(SerializerProvider prov, Object value) {
        String str = (String) value;
        return str.isEmpty();
    }

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

    @Override
    public void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
        this.serialize(value, gen, provider);
    }

    @Override
    public JsonNode getSchema(SerializerProvider provider, Type typeHint) {
        return this.createSchemaNode("string", true);
    }

    @Override
    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
        this.visitStringFormat(visitor, typeHint);
    }
}

3. DataMaskingOperation Interface and Strategies

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

public enum DataMaskingFunc {
    /** No masking */
    NO_MASK((str, maskChar) -> str),
    /** Mask every character */
    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();
        }
        return str;
    });

    private final DataMaskingOperation operation;
    DataMaskingFunc(DataMaskingOperation operation) { this.operation = operation; }
    public DataMaskingOperation operation() { return this.operation; }
}

4. Custom AnnotationIntrospector

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

5. Overriding the ObjectMapper Configuration

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

6. Applying the Annotation to Domain Objects

public class User implements Serializable {
    /** Primary key */
    private Long id;

    /** Name – fully masked */
    @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
    private String name;

    /** Age – no masking */
    private Integer age;

    /** Email – fully masked */
    @DataMasking(maskFunc = DataMaskingFunc.ALL_MASK)
    private String email;
}

With this setup, any field annotated with @DataMasking will be automatically processed by Jackson during serialization, applying the selected masking strategy without additional code in controllers or services.

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 BootCustom AnnotationJacksondata masking
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.