Customize Jackson ObjectMapper to Read custom Annotation and mask fields annotated
package stackoverflow;import static org.hamcrest.MatcherAssert.assertThat;import static org.hamcrest.Matchers.is;import java.io.IOException;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import org.hamcrest.Matchers;import org.junit.Test;import com.fasterxml.jackson.core.JsonGenerator;import com.fasterxml.jackson.core.JsonParser;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.AnnotationIntrospector;import com.fasterxml.jackson.databind.DeserializationContext;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.SerializerProvider;import com.fasterxml.jackson.databind.deser.std.StdDeserializer;import com.fasterxml.jackson.databind.introspect.Annotated;import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;import com.fasterxml.jackson.databind.ser.std.StdSerializer;public class MaskingAnnotationExample { // Define @custom Annotation // assumed to be used by String type field for this example @Retention(RetentionPolicy.RUNTIME) static @interface MaskSensitiveData { } public static class MyBean { private String userName; @MaskSensitiveData private String cardNumber; public MyBean() { } public String getCardNumber() { return cardNumber; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; } } // map the Serializer/Deserializer based on custom annotation public static class MaskSensitiveDataAnnotationIntrospector extends NopAnnotationIntrospector { private static final long serialVersionUID = 1L; @Override public Object findSerializer(Annotated am) { MaskSensitiveData annotation = am.getAnnotation(MaskSensitiveData.class); if (annotation != null) { return MaskSensitiveDataSerializer.class; } return null; } @Override public Object findDeserializer(Annotated am) { MaskSensitiveData annotation = am.getAnnotation(MaskSensitiveData.class); if (annotation != null) { return MaskSensitiveDataDeserializer.class; } return null; } } public static class MaskSensitiveDataDeserializer extends StdDeserializer<String> { private static final long serialVersionUID = 1L; public MaskSensitiveDataDeserializer() { super(String.class); } @Override public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { // un-masking logic here. in our example we are removing "MASK" // string String s = p.getValueAsString(); return s.substring(4); } } public static class MaskSensitiveDataSerializer extends StdSerializer<String> { private static final long serialVersionUID = 1L; public MaskSensitiveDataSerializer() { super(String.class); } @Override public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException { // Masking data; for our example we are adding 'MASK' gen.writeString("MASK" + value); } } @Test public void demo() throws Exception { ObjectMapper mapper = new ObjectMapper(); AnnotationIntrospector sis = mapper.getSerializationConfig().getAnnotationIntrospector(); AnnotationIntrospector dis = mapper.getDeserializationConfig().getAnnotationIntrospector(); AnnotationIntrospector is1 = AnnotationIntrospectorPair.pair(sis, new MaskSensitiveDataAnnotationIntrospector()); AnnotationIntrospector is2 = AnnotationIntrospectorPair.pair(dis, new MaskSensitiveDataAnnotationIntrospector()); mapper.setAnnotationIntrospectors(is1, is2); MyBean obj = new MyBean(); obj.setUserName("Saurabh Bhardwaj"); obj.setCardNumber("4455-7788-9999-7777"); String json = mapper.writeValueAsString(obj); String expectedJson = "{\"userName\":\"Saurabh Bhardwaj\",\"cardNumber\":\"MASK4455-7788-9999-7777\"}"; assertThat(json, Matchers.is(expectedJson)); MyBean cloned = mapper.readValue(json, MyBean.class); assertThat(cloned.getCardNumber(), is(obj.getCardNumber())); }}
Hope this helps.
Here is a solution to your problem using custom JsonSerializer.Steps are followed from this blog post.
Create a custom serializer
public class MaskingSerializer extends JsonSerializer < MyBean > { @ Override public void serialize(MyBean value, JsonGenerator jGen, SerializerProvider serializers) throws IOException, JsonProcessingException { jGen.writeStartObject(); Field[] fields = value.getClass().getDeclaredFields(); for (Field field: fields) { field.setAccessible(true); MaskSensitiveData mask = field.getDeclaredAnnotation(MaskSensitiveData.class); try { if (mask != null) { field.setAccessible(true); field.set(value, field.get(value).toString().replaceAll(".", "*")); } jGen.writeStringField(field.getName(), field.get(value).toString()); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } jGen.writeEndObject(); }}
Create a module to bundle the serializer
public class MaskingModule extends SimpleModule { private static final String NAME = "CustomIntervalModule"; private static final VersionUtil VERSION_UTIL = new VersionUtil() {}; public MaskingModule() { super(NAME, VERSION_UTIL.version()); addSerializer(MyBean.class, new MaskingSerializer()); }}
Register the module with ObjectMapper.
public class CustomObjectMapper extends ObjectMapper { public CustomObjectMapper() { registerModule(new MaskingModule()); } }
Test the code
public class MyBeanTest { private static final CustomObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); @Test public void testIntervalSerialization() throws Exception { MyBean mb = new MyBean(); mb.setAbc("value"); mb.setCardNumber("4441114443335551"); mb.setUserName("User"); mb.setXyz("value"); String result = OBJECT_MAPPER.writeValueAsString(mb); System.out.println(result); String expected = "{\"userName\":\"User\",\"cardNumber\":\"****************\",\"abc\":\"value\",\"xyz\":\"value\"}"; Assert.assertEquals(expected, result); }}
I am using a GENERAL & SHARED ObjectMapper
that configured by Spring boot, and I don't want it got polluted by setSerializerFactory()
or rewrite the whole BeanSerializer
. Here comes my solution:
Configure ObjectMapper
@Configuration @AutoConfigureAfter(JacksonAutoConfiguration.class) public static class ExtJacksonConfig { @Autowired private ObjectMapper objectMapper; @PostConstruct public void postConstruct() throws JsonMappingException { SimpleModule module = new SimpleModule(); module.addSerializer(ProductOrder.class, new POProductOrderSerializer( (BeanSerializerBase) objectMapper.getSerializerFactory().createSerializer( objectMapper.getSerializerProviderInstance(), objectMapper.getSerializationConfig().constructType(ProductOrder.class)))); objectMapper.registerModule(module); } }
A General SensitiveDataSerializer to mask sensitive fields
public class SensitiveDataSerializer<T> extends BeanSerializer { private final Function<T, Boolean> authorityChecker; private final String maskText; public SensitiveDataSerializer(BeanSerializerBase src, Function<T, Boolean> authorityChecker, String maskText) { super(src); this.authorityChecker = authorityChecker; this.maskText = Optional.ofNullable(maskText).orElse("****"); assert(this.authorityChecker != null); assert(!Checker.isEmpty(sensitiveFieldNames)); // Replace BeanPropertyWriter for (int i=0; i<_props.length; i++) { if (_props[i] != null && _props[i].getAnnotation(MaskSensitiveData.class) != null) { _props[i] = new SensitivePropertyWriter(_props[i]); } } for (int j=0; j<_filteredProps.length; j++) { if (_filteredProps[j] != null && _filteredProps[j].getAnnotation(MaskSensitiveData.class) != null) { _filteredProps[j] = new SensitivePropertyWriter(_filteredProps[j]); } } } class SensitivePropertyWriter extends BeanPropertyWriter { private final BeanPropertyWriter writer; SensitivePropertyWriter(BeanPropertyWriter writer) { super(writer); this.writer = writer; } @Override public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception { if (authorityChecker.apply((T) bean)) { super.serializeAsField(bean, gen, prov); return; } gen.writeStringField(writer.getName(), maskText); } }}
Finally, the concrete Serializer you need
public class ProductOrderSerializer extends SensitiveDataSerializer<ProductOrder> { public POProductOrderSerializer(BeanSerializerBase src) { super(src, (productOrder -> { return true; // put your permission checking code here }), "****"); }}