javajsonspringobjectmapper

How to censor custom annotated fields in java dtos?


I need a handy way to censore DTO fields/secrets when converting them into a json string (for logging).

Im using objectmapper and I heard about @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) but it completely hides the annotated field and it can be confusing because if I want to trace an error in the logs I might think that it was just null, therefore It would be better if the property's value was simply replaced with something like ***.

Defining a list of field names to replace values is also not suitable because i might censore fields that i dont want to. Also refactoring would complicate things too.

My idea is to annotate fields with a custom annotation and implement some json postprocessing with objectmapper but i couldnt find a way to do it. For example:

public record Person(String name, @Censored String secret) {
}

// and later
log.info(objectMapper.writeValueAsString(person));

The output should be

{"name":"John","secret":"***"}

Is there a way to configure ObjectMapper to censore fields that are annotated with my custom @Censored annotation?

Im also open to other suggestions to implement log censoring logic.

I tried to use @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) but it didnt achieve the result that i wanted.


Solution

  • The simplest way I can think to implement this via Jackson would be with a custom serializer:

    public class CensoredSerializer<T> extends StdSerializer<T> {
        public CensoredSerializer() {
            this(null);
        }
    
        public CensoredSerializer(Class<T> t) {
            super(t);
        }
    
        @Override
        public void serialize(T value, JsonGenerator gen, SerializerProvider provider)
                throws IOException
        {
            gen.writeString("***");
        }
    }
    

    You would use this like so:

    public record Person(String name, @JsonSerialize(using = CensoredSerializer.class) String secret) {
    }
    

    If that's too verbose for you, you can use it as the basis for a custom annotation:

    @Retention(RetentionPolicy.RUNTIME)
    @JacksonAnnotationsInside
    @JsonSerialize(using = CensoredSerializer.class)
    public @interface Censored {
    }
    

    This lets you use @Censored as a shorthand for @JsonSerialize(using = CensoredSerializer.class), like so:

    public record Person(String name, @Censored String secret) {
    }
    

    As mentioned in the comments, it is also possible to do this by masking values in the logging config. I personally feel like there's some value in having it defined directly in the class instead, but your milage may vary!