Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Customize Jackson ObjectMapper to Read custom Annotation and mask fields annotated

I have a requirement where I have created a Custom Annotation @MaskSensitiveData. I annotate sensitive fields. like

class MyBean {
    String userName;
    @MaskSensitiveData
    String cardNumber;
    String abc;
    String xyz;
}

ObjectMapper mapper = new ObjectMapper();
    String json = null;
    AnnotationIntrospector primary = new JaxbAnnotationIntrospector();
    AnnotationIntrospector secondary = new JacksonAnnotationIntrospector();
    AnnotationIntrospector pair = new AnnotationIntrospectorPair(primary, secondary);
    mapper.setAnnotationIntrospector(pair);
    try {
        json = mapper.writeValueAsString(obj);
        /*
         * if(json != null ) { json = getLoggableString(json); }
         */
    } catch (Exception e) {
        return "Unable to convert to Json object:" + obj.toString() + " Message: " + e.getMessage();

    }

I am using Jackson ObjectMapper to convert objct to Json like. I need to customize Object Mapper to mask cardNumber field in return json. Please suggest a better way.

like image 505
Saurabh Bhardwaj Avatar asked Jan 23 '16 15:01

Saurabh Bhardwaj


People also ask

Should I declare Jackson's ObjectMapper as a static field?

Yes, that is safe and recommended.

What does Jackson ObjectMapper do?

The Jackson ObjectMapper can parse JSON from a string, stream or file, and create a Java object or object graph representing the parsed JSON. Parsing JSON into Java objects is also referred to as to deserialize Java objects from JSON. The Jackson ObjectMapper can also create JSON from Java objects.

What does ObjectMapper readValue do?

The simple readValue API of the ObjectMapper is a good entry point. We can use it to parse or deserialize JSON content into a Java object. Also, on the writing side, we can use the writeValue API to serialize any Java object as JSON output.

What is the use of ObjectMapper in spring boot?

Overview. When using JSON format, Spring Boot will use an ObjectMapper instance to serialize responses and deserialize requests. In this tutorial, we'll take a look at the most common ways to configure the serialization and deserialization options. To learn more about Jackson, be sure to check out our Jackson tutorial.


2 Answers

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.

like image 63
skadya Avatar answered Sep 20 '22 16:09

skadya


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);
    }
}
like image 41
Arun A Avatar answered Sep 18 '22 16:09

Arun A