Let's say you have an annotation configured on a per field basis:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DoNotEncrypt {
}
OK so now you have the annotation. So you can have your fields annotated when you don't want them encrypted:
static class CoolStuff {
private String shouldBeEncrypted;
@DoNotEncrypt
private String notEncrypted = "should not be encrypted";
public String getShouldBeEncrypted() {
return shouldBeEncrypted;
}
public void setShouldBeEncrypted(String shouldBeEncrypted) {
this.shouldBeEncrypted = shouldBeEncrypted;
}
public String getNotEncrypted() {
return notEncrypted;
}
public void setNotEncrypted(String notEncrypted) {
this.notEncrypted = notEncrypted;
}
}
I want to implement this serializer:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Encrypt every string except fields that have annotation {@link DoNotEncrypt}
*/
public class ConfidentialSerializer extends JsonSerializer<Object> {
private static final Logger LOG = LoggerFactory.getLogger(ConfidentialSerializer.class);
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) {
// I want to implement this here
}
public static ObjectMapper configurationObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
// is this right?
module.addSerializer(Object.class, new ConfidentialSerializer());
objectMapper.registerModule(module);
return objectMapper;
}
}
In order to test the change i want to see that strings are encrypted unless annotationed @DoNotEncrypt
.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
public class ConfidentialSerializerTest {
static class CoolStuff {
private String shouldBeEncrypted;
@DoNotEncrypt
private String notEncrypted;
public String getShouldBeEncrypted() {
return shouldBeEncrypted;
}
public void setShouldBeEncrypted(String shouldBeEncrypted) {
this.shouldBeEncrypted = shouldBeEncrypted;
}
public String getNotEncrypted() {
return notEncrypted;
}
public void setNotEncrypted(String notEncrypted) {
this.notEncrypted = notEncrypted;
}
}
@Test
public void testConfigurationObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = ConfidentialSerializer.configurationObjectMapper();
CoolStuff coolStuff = new CoolStuff();
coolStuff = new CoolStuff();
coolStuff.setShouldBeEncrypted("is");
coolStuff.setNotEncrypted("is not");
String out = objectMapper.writeValueAsString(coolStuff);
Map mapOut = objectMapper.readValue(out, Map.class);
Assert.assertNotEquals("is", mapOut.get("notEncrypted"));
Assert.assertEquals("is not", mapOut.get("shouldBeEncrypted")); }
}
Is this something that is possible with Object Mapper? If so, how do you do it?
I think I have it working now.
The key is recursion in ConfidentialSerializer
. Once you have that working the rest is pretty easy.
DoNotEncrypt.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DoNotEncrypt {
}
ConfidentialSerializerTest.java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
public class ConfidentialSerializer extends JsonSerializer<Object> {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
try {
if (value == null) {
gen.writeNull();
return;
}
Class<?> clazz = value.getClass();
// Write String values directly
if (value instanceof String) {
gen.writeString((String) value);
return;
}
if (value instanceof Long) {
gen.writeNumber((Long) value);
return;
}
if (value instanceof Integer) {
gen.writeNumber((Integer) value);
return;
}
if (value instanceof Short) {
gen.writeNumber((Short) value);
return;
}
if (value instanceof Boolean) {
gen.writeBoolean((Boolean) value);
return;
}
if (value instanceof Collection) {
gen.writeStartArray();
for (Object item : (Collection<?>) value) {
serialize(item, gen, serializers); // Recursively process list elements
}
gen.writeEndArray();
return;
}
if (value instanceof Map) {
gen.writeStartObject();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
gen.writeFieldName(entry.getKey().toString());
serialize(entry.getValue(), gen, serializers); // Recursively process map values
}
gen.writeEndObject();
return;
}
gen.writeStartObject();
for (Field field : clazz.getDeclaredFields()) {
Method getterMethod = lookupGetterMethod(value, field);
if (getterMethod != null) {
Object fieldValue = getFieldValue(getterMethod, field.getName(), value);
gen.writeFieldName(field.getName());
// Recursively process child fields
if (fieldValue != null) {
if (field.isAnnotationPresent(Confidential.class) && fieldValue instanceof String) {
fieldValue = AESEncrypter.encrypt((String) fieldValue);
}
serialize(fieldValue, gen, serializers);
} else {
gen.writeNull();
}
}
}
gen.writeEndObject();
} catch (Exception e) {
throw new RuntimeException("Error encrypting nested field", e);
}
}
private static @Nullable Method lookupGetterMethod(Object value, Field field) {
String fieldName = field.getName();
String methodNameGet = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
String methodNameIs = "is" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
Method getterMethod = Arrays.stream(value.getClass().getMethods())
.filter(method -> method.getParameterCount() == 0 &&
method.getModifiers() == java.lang.reflect.Modifier.PUBLIC &&
(method.getName().equals(methodNameGet) || method.getName().equals(methodNameIs)))
.findFirst().orElse(null);
return getterMethod;
}
public Object getFieldValue(Method getterMethod, String fieldName, Object object) {
try {
return getterMethod.invoke(object);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}