Mask json fields using jackson - java

I am trying to mask sensitive data while serializing using jackson.
I have tried using #JsonSerialize and a custom annotation #Mask .
Mask.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 Mask {
String value() default "XXX-DEFAULT MASK FORMAT-XXX";
}
Employee.java
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.util.Map;
public class Employee {
#Mask(value = "*** The value of this attribute is masked for security reason ***")
#JsonSerialize(using = MaskStringValueSerializer.class)
protected String name;
#Mask
#JsonSerialize(using = MaskStringValueSerializer.class)
protected String empId;
#JsonSerialize(using = MaskMapStringValueSerializer.class)
protected Map<Category, String> categoryMap;
public Employee() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmpId() {
return empId;
}
public void setEmpId(String empId) {
this.empId = empId;
}
public Map<Category, String> getCategoryMap() {
return categoryMap;
}
public void setCategoryMap(Map<Category, String> categoryMap) {
this.categoryMap = categoryMap;
}
}
Category.java
public enum Category {
#Mask
CATEGORY1,
#Mask(value = "*** This value of this attribute is masked for security reason ***")
CATEGORY2,
CATEGORY3;
}
MaskMapStringValueSerializer.java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.Map;
public class MaskMapStringValueSerializer extends JsonSerializer<Map<Category, String>> {
#Override
public void serialize(Map<Category, String> map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
for (Category key : map.keySet()) {
Mask annot = null;
try {
annot = key.getClass().getField(key.name()).getAnnotation(Mask.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (annot != null) {
jsonGenerator.writeStringField(((Category) key).name(), annot.value());
} else {
jsonGenerator.writeObjectField(((Category) key).name(), map.get(key));
}
}
jsonGenerator.writeEndObject();
}
}
MaskStringValueSerializer.java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
public class MaskStringValueSerializer extends StdSerializer<String> implements ContextualSerializer {
private Mask annot;
public MaskStringValueSerializer() {
super(String.class);
}
public MaskStringValueSerializer(Mask logMaskAnnotation) {
super(String.class);
this.annot = logMaskAnnotation;
}
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (annot != null && s != null && !s.isEmpty()) {
jsonGenerator.writeString(annot.value());
} else {
jsonGenerator.writeString(s);
}
}
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
Mask annot = null;
if (beanProperty != null) {
annot = beanProperty.getAnnotation(Mask.class);
}
return new MaskStringValueSerializer(annot);
}
}
MaskValueTest.java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class MaskValueTest {
public static void main(String args[]) throws Exception{
Employee employee = new Employee();
employee.setName("John Doe");
employee.setEmpId("1234567890");
Map<Category, String> catMap = new HashMap<>();
catMap.put(Category.CATEGORY1, "CATEGORY1");
catMap.put(Category.CATEGORY2, "CATEGORY2");
catMap.put(Category.CATEGORY3, "CATEGORY3");
employee.setCategoryMap(catMap);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));
}
}
Output -
{
"name" : "*** The value of this attribute is masked for security reason ***",
"empId" : "XXX-DEFAULT MASK FORMAT-XXX",
"categoryMap" : {
"CATEGORY1" : "XXX-DEFAULT MASK FORMAT-XXX",
"CATEGORY2" : "*** The value of this attribute is masked for security reason ***",
"CATEGORY3" : "CATEGORY3"
}
}
The result is as per expectation, however, this seems to be static masking.
The intention was to mask only when needed, e.g. while printing in the logs where the all these sensitive data should be masked.
If I have to send this json for document indexing where the values should be as it is, this implementation fails.
I am looking for an Annotation based solution, where I can use 2 different instance of ObjectMapper initialized with JsonSerializers.

This can be an implementation for what Andreas suggested:
create a class MaskAnnotationIntrospector which extend from JacksonAnnotationIntrospector and override its findSerializer method, like this:
public class MaskAnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findSerializer(Annotated am) {
Mask annotation = am.getAnnotation(Mask.class);
if (annotation != null)
return MaskingSerializer.class;
return super.findSerializer(am);
}
}
Therefore, you can have two instance of ObjectMapper. Add MaskAnnotationIntrospector to the one in which you want to Mask (e.g. for logging purpose):
mapper.setAnnotationIntrospector(new MaskAnnotationIntrospector());
The other instance which MaskAnnotationIntrospector has not set into it, do not mask any during serialization.
P.S. MaskAnnotationIntrospector can be extended from both JacksonAnnotationIntrospector & NopAnnotationIntrospector, but the latter does not provide any implementation for findSerializer method and calling super.findSerializer(am) simply return null and as a direct result, other Jackson annotation (such as #JsonIgnore) discarded, but by using the former, this problem solved

Remove the #JsonSerialize annotations, and put the logic of how to handle the #Mask annotation in a Module, e.g. have it add an AnnotationIntrospector.
You can now choose whether or not to call registerModule(Module module).
As for writing the module, I'll leave that up to you. If you have any questions about that, ask another Question.

Instead of having MaskStringValueSerializer.java you can create module to bundle the serializer and register the module with objectmapper whenever you want , which will eventually allow you to have two different instances of objectmapper.
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 MaskMapStringValueSerializer());
}
}
Register the module with ObjectMapper and use it
ObjectMapper objectMapper = new ObjectMapper().registerModule(new MaskingModule());
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));
Also you can extend the Object Mapper , register the module and use it
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
registerModule(new MaskingModule());
}
}
CustomObjectMapper customObjectMapper = new CustomObjectMapper ();
System.out.println(customObjectMapper .writerWithDefaultPrettyPrinter().writeValueAsString(employee));

why don't you use two parameters one for original value and one for masked value. For example in this case you can use String name and String maskedName. then for logging you can use masked value.

Related

Jackson: add suffix according to a field type

Here my POJO:
public class AutorDenormalized {
private String id;
private Long unitatId;
private String grupId;
private String descripcio;
public AutorDenormalized() {
}
// getters $ setters
}
I'd like to serialise this kind of objects adding a suffix according to field type. I mean,
If field type is a String -> then add a *_s suffix
If field type is a Long -> then add a *_l suffix
Otherwise keep going
Do you have any ideas how to solve it?
You need to implement custom BeanPropertyWriter which can generate property name with a suffix. To register custom BeanPropertyWriter you need to create custom BeanSerializerModifier.
Below example shows simplified implementation which shows a way how to achieve above result:
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.util.NameTransformer;
import java.io.IOException;
import java.util.List;
public class JsonTypeInfoApp {
public static void main(String[] args) throws IOException {
SimpleModule typeSuffixModule = new SimpleModule();
typeSuffixModule.setSerializerModifier(new TypeSuffixBeanSerializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.registerModule(typeSuffixModule);
System.out.println(mapper.writeValueAsString(new AutorDenormalized()));
}
}
class TypeSuffixBeanSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (int i = 0; i < beanProperties.size(); ++i) {
final BeanPropertyWriter writer = beanProperties.get(i);
Class<?> rawType = writer.getType().getRawClass();
if (supports(rawType)) {
final String suffix = constructSuffix(rawType);
beanProperties.set(i, writer.rename(NameTransformer.simpleTransformer(null, suffix)));
}
}
return beanProperties;
}
private String constructSuffix(Class<?> rawType) {
return "_" + Character.toLowerCase(rawType.getSimpleName().charAt(0));
}
private boolean supports(Class<?> rawClass) {
return rawClass == String.class || rawClass == Long.class;
}
}
Above code prints:
{
"id_s" : "1",
"unitatId_l" : 123,
"grupId_s" : "2",
"descripcio_s" : "3"
}
See also:
Jackson custom serialization and deserialization
Aside from the accepted answer, which works fine, you could also consider implementing PropertyNameStrategy: it would let you rename properties and gets field, setter/getter, creator parameter (which you need to find type of property). Might be little bit less work.

Jackson won't serialize null with a custom Serializer

I have a custom bean serializer that I'd like to apply, but when I do, Jackson no longer includes null properties.
The following code reproduces the issue:
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import lombok.Value;
public class Test {
#Value
public static class Contact {
String first;
String middle;
String last;
String email;
}
public static void main(String[] args) throws Exception {
Contact contact = new Contact("Bob", null, "Barker", null);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new SimpleModule() {
#Override public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription desc, JsonSerializer<?> serializer) {
// return serializer;
return new JsonSerializer<Object>() {
#Override public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
((JsonSerializer<Object>) serializer).serialize(value, gen, serializers);
}};
}
});
}
});
System.out.println(
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(contact)
);
}
}
The above code does nothing other that register a 'custom' serializer (that just delegates back to the original serializer), yet it produces JSON without the null properties:
{ "first" : "Bob", "last" : "Barker" }
If you comment out the return new JsonSerializer<Object>() {... and return the passed in serializer as is return serializer;, then Jackson serializes the null properties:
{ "first" : "Bob", "middle" : null, "last" : "Barker", "email"
: null }
I have read over many seemingly related SO articles, but none have led me to a solution yet. I've tried explicitly setting the mapper to Include.ALWAYS on serialization, with no luck.
My only lead is a comment in the JavaDoc for JsonSerializer:
NOTE: various serialize methods are never (to be) called
with null values -- caller must handle null values, usually
by calling {#link SerializerProvider#findNullValueSerializer} to obtain
serializer to use.
This also means that custom serializers cannot be directly used to change
the output to produce when serializing null values.
I am using Jackson version 2.11.2.
My question is: How can I write a custom serializer and have Jackson respect its usual Include directives with regard to null property serialization?
Context Info: My actual custom serializer's job is to conditionally hide properties from serialization. I have a custom annotation, #JsonAuth that is meta-annotated with #JacksonAnnotationsInside #JsonInclude(Include.NON_EMPTY) which my custom serializer (a ContextualSerializer) looks for in an overriden isEmpty method and returns true (treat as empty) if authorization is lacking. The end result is that I have an annotation that can be applied to properties which will hide the property from serialization if the client is not authorized. Except ... usage of the custom serializer has the unintended side effect of dropping all null properties.
Update: Jackson's BeanPropertyWriter.serializeAsField(...) method will completely ignore any custom serializer assigned to the property if the value is null.
I was able to override this behavior by writing a small extension to the class, which allowed my "isAuthorized" logic to preempt the null check:
public class JsonAuthPropertyWriter extends BeanPropertyWriter {
private final Predicate<Object> authFilter;
private JsonAuthPropertyWriter(BeanPropertyWriter delegate, Predicate<Object> authFilter) {
super(delegate);
this.authFilter = authFilter;
// set null serializer or authorized null values disappear
super.assignNullSerializer(NullSerializer.instance);
}
#Override
public void serializeAsField(
Object bean,
JsonGenerator gen,
SerializerProvider prov) throws Exception {
boolean authorized = authFilter.test(bean);
if (!authorized) return;
super.serializeAsField(bean, gen, prov);
}
}
And I injected these custom BeanPropertyWriters using a BeanSerializerModifier:
private static class JsonAuthBeanSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(
SerializationConfig config,
BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties
) {
for (int i = 0; i < beanProperties.size(); i++) {
BeanPropertyWriter beanPropertyWriter = beanProperties.get(i);
JsonAuth jsonAuth = beanPropertyWriter.findAnnotation(JsonAuth.class);
if (jsonAuth != null) {
Predicate<Object> authPredicate = ...
beanProperties.set(i, new JsonAuthPropertyWriter(beanPropertyWriter, authPredicate));
}
}
return beanProperties;
}
}
I may be misunderstanding what you want, but this approach seems useful:
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.Map;
public class Test2 {
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#interface JsonAuth {
}
#JsonFilter("myFilter")
public static class Contact {
#JsonAuth
String first;
#JsonAuth
String middle;
#JsonAuth
String last;
String email;
public Contact(String first, String middle, String last, String email) {
this.first = first;
this.middle = middle;
this.last = last;
this.email = email;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getMiddle() {
return middle;
}
public void setMiddle(String middle) {
this.middle = middle;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public static Map<String,Boolean> fieldSerialisationCount = new HashMap<>();
public static void main(String[] args) throws Exception {
Contact contact = new Contact("Bob", null, "Barker", null);
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider().addFilter("myFilter", new SimpleBeanPropertyFilter() {
#Override
protected boolean include(BeanPropertyWriter writer) {
return super.include(writer) && isAuthed(writer);
}
#Override
protected boolean include(PropertyWriter writer) {
return super.include(writer) && isAuthed(writer);
}
private boolean isAuthed(PropertyWriter writer) {
if (!writer.getMember().hasAnnotation(JsonAuth.class)) {
return true;
} else {
return fieldSerialisationCount.compute(writer.getName(), (n, b) -> b == null ? true : !b); // check auth here
}
}
});
mapper.setFilterProvider(filters);
ObjectWriter writer = mapper.writer(filters).withDefaultPrettyPrinter();
System.out.println(
writer.writeValueAsString(contact)
);
System.out.println(
writer.writeValueAsString(contact)
);
System.out.println(
writer.writeValueAsString(contact)
);
}
}
It serialises annotated fields every other time, just as an example of a filter using persistent state.
Please let me know whether this works for you.
By the way, I agree that Jackson has the problem you describe, and I don't know how to solve it, so this is a work-around rather than an answer to your original question.

Deserialize to String or Object using Jackson

I have an object that sometimes looks like this:
{
"foo" : "bar",
"fuzz" : "bla"
}
and sometimes looks like this:
{
"foo" : { "value" : "bar", "baz": "asdf" },
"fuzz" : { "thing" : "bla", "blip" : "asdf" }
}
these classes would look like:
public class Foo {
String value;
String baz;
}
public class Fuzz {
String thing;
String blip;
}
where the first cases are shorthand for the second ones. I would like to always deserialize into the second case.
Further - this is a pretty common pattern in our code, so I would like to be able to do the serialization in a generic manner, as there are other classes similar to Foo above that have the same pattern of using String as a syntactic sugar for a more complex object.
I'd imagine the code to use it would look something like this
public class Thing {
#JsonProperty("fuzz")
Fuzz fuzz;
#JsonProperty("foo")
Foo foo;
}
How do I write a custom deserializer (or some other module) that generically handles both cases?
To make it generic we need to be able to specify name which we would like to set in object for JSON primitive. Some flexibility gives annotation approach. Let's define simple annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
#interface JsonPrimitiveName {
String value();
}
Name means: in case primitive will appear in JSON use value() to get property name for given primitive. It binds JSON primitive with POJO field. Simple deserialiser which handles JSON object and JSON primitive:
class PrimitiveOrPojoJsonDeserializer extends JsonDeserializer implements ContextualDeserializer {
private String primitiveName;
private JavaType type;
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonDeserializer<Object> deserializer = ctxt.findRootValueDeserializer(type);
if (p.currentToken() == JsonToken.START_OBJECT) {
return deserializer.deserialize(p, ctxt);
} else if (p.currentToken() == JsonToken.VALUE_STRING) {
BeanDeserializer beanDeserializer = (BeanDeserializer) deserializer;
try {
Object instance = beanDeserializer.getValueInstantiator().getDefaultCreator().call();
SettableBeanProperty property = beanDeserializer.findProperty(primitiveName);
property.deserializeAndSet(p, ctxt, instance);
return instance;
} catch (Exception e) {
throw JsonMappingException.from(p, e.getMessage());
}
}
return null;
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
JsonPrimitiveName annotation = property.getAnnotation(JsonPrimitiveName.class);
PrimitiveOrPojoJsonDeserializer deserializer = new PrimitiveOrPojoJsonDeserializer();
deserializer.primitiveName = annotation.value();
deserializer.type = property.getType();
return deserializer;
}
}
Now we need to annotate POJO fields as below:
class Root {
#JsonPrimitiveName("value")
#JsonDeserialize(using = PrimitiveOrPojoJsonDeserializer.class)
private Foo foo;
#JsonPrimitiveName("thing")
#JsonDeserialize(using = PrimitiveOrPojoJsonDeserializer.class)
private Fuzz fuzz;
// getters, setters
}
I assume that all classes are POJO-s and follow all rules - have getters, setters and default constructor. In case constructor does not exist you need to change this beanDeserializer.getValueInstantiator().getDefaultCreator().call() line somehow which fits your requirements.
Example app:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(jsonFile, Root.class));
}
}
Prints for shortened JSON:
Root{foo=Foo{value='bar', baz='null'}, fuzz=Fuzz{thing='bla', blip='null'}}
And for full JSON payload:
Root{foo=Foo{value='bar', baz='asdf'}, fuzz=Fuzz{thing='bla', blip='asdf'}}

Jackson custom serialization and deserialization

i'm unable to figure out the proper way to implement the custom serialization/deserialization with jackson.
I have many classes (~50) with primitive fields that should be serialized/deserialized not as primitives.
like:
class User {
int height // this field should be serialized as "height": "10 m"
}
class Food {
int temperature // this field should be serialized as "temperature": "50 C"
}
class House {
int width // this field should be serialized as "width": "10 m"
}
all serializations and deserializations are very similar, I just need to add a suffix after the integer (C, pages, meters, etc..)
A straightforward way to do this is to put a pair of #JsonSerialize/#JsonDeserialize annotation to each such field and implement them.
But i will end up with 100 very similar serializers / deserializers.
I thought about adding custom annotation to each field, say #Units("Degree") or #Units("Meters"), to such integer fields and implement a SerializationProvider that will create serializers in a generic way based on an annotation value.
But I didn't find a place where the information about the property annotations is available.
Idea with Unit annotation is really good. We need to only add custom com.fasterxml.jackson.databind.ser.BeanSerializerModifier and com.fasterxml.jackson.databind.ser.BeanPropertyWriter implementations. Let's create first our annotation class:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
#interface Unit {
String value();
}
POJO model could look like below:
class Pojo {
private User user = new User();
private Food food = new Food();
private House house = new House();
// getters, setters, toString
}
class User {
#Unit("m")
private int height = 10;
// getters, setters, toString
}
class Food {
#Unit("C")
private int temperature = 50;
// getters, setters, toString
}
class House {
#Unit("m")
private int width = 10;
// getters, setters, toString
}
Having all of that we need to customise property serialisation:
class UnitBeanSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (int i = 0; i < beanProperties.size(); ++i) {
final BeanPropertyWriter writer = beanProperties.get(i);
AnnotatedMember member = writer.getMember();
Unit units = member.getAnnotation(Unit.class);
if (units != null) {
beanProperties.set(i, new UnitBeanPropertyWriter(writer, units.value()));
}
}
return beanProperties;
}
}
class UnitBeanPropertyWriter extends BeanPropertyWriter {
private final String unit;
protected UnitBeanPropertyWriter(BeanPropertyWriter base, String unit) {
super(base);
this.unit = unit;
}
#Override
public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {
gen.writeFieldName(_name);
final Object value = (_accessorMethod == null) ? _field.get(bean) : _accessorMethod.invoke(bean, (Object[]) null);
gen.writeString(value + " " + unit);
}
}
Using SimpleModule we can register it and use with ObjectMapper:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
public class JsonApp {
public static void main(String[] args) throws Exception {
SimpleModule unitModule = new SimpleModule();
unitModule.setSerializerModifier(new UnitBeanSerializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(unitModule);
Pojo pojo = new Pojo();
System.out.println(mapper.writeValueAsString(pojo));
}
}
prints:
{
"user" : {
"height" : "10 m"
},
"food" : {
"temperature" : "50 C"
},
"house" : {
"width" : "10 m"
}
}
Of course, you need to test it and handle all corner cases but above example shows general idea. In the similar way we can handle deserialisation. We need to implement custom BeanDeserializerModifier and one custom UnitDeserialiser:
class UnitBeanDeserializerModifier extends BeanDeserializerModifier {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
JsonDeserializer<?> jsonDeserializer = super.modifyDeserializer(config, beanDesc, deserializer);
if (jsonDeserializer instanceof StdScalarDeserializer) {
StdScalarDeserializer scalarDeserializer = (StdScalarDeserializer) jsonDeserializer;
Class scalarClass = scalarDeserializer.handledType();
if (int.class == scalarClass) {
return new UnitIntStdScalarDeserializer(scalarDeserializer);
}
}
return jsonDeserializer;
}
}
and example deserialiser for int:
class UnitIntStdScalarDeserializer extends StdScalarDeserializer<Integer> {
private StdScalarDeserializer<Integer> src;
public UnitIntStdScalarDeserializer(StdScalarDeserializer<Integer> src) {
super(src);
this.src = src;
}
#Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getValueAsString();
String[] parts = value.split("\\s+");
if (parts.length == 2) {
return Integer.valueOf(parts[0]);
}
return src.deserialize(p, ctxt);
}
}
Above implementation is just an example and should be improved for other primitive types. We can register it in the same way using simple module. Reuse the same as for serialisation:
unitModule.setDeserializerModifier(new UnitBeanDeserializerModifier());

jackson Pascal Case and #JsonProperty on getter

i create a mapper with
new ObjectMApper()
.setPropertyNamingStrategy(PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE)
.setSerializationInclusion(Include.NON_NULL)
and serialization works perfectly on fields (no getters and setters). field currentStatus is serialized to "currentStatus" (first letter uppercase). but i have also one getter (without a field and setter) which must be camelCase. so i do:
#JsonProperty("abcDef")
public String getZxy() {...
but it is serialized to "AbcDef" instead of "abcDef". it looks like naming strategy still triggers and change the first letter. i use jackson-databind 2.3.2;
how can i map this getter with first letter lowercase?
EDIT:
ugly code, but shows the problem. this test should pass but it fails
import static org.assertj.core.api.Assertions.assertThat;
import org.testng.annotations.Test;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
public class JsonFailureTest {
#Test
public void should_serialize_first_letter_lowercase() throws Exception {
String json = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE)
.writeValueAsString(
new Object(){
#JsonProperty("fooBar")
public String whatever() {return "";}
});
assertThat(json).contains("fooBar");
}
}
Here's a workaround using a custom "annotation-aware" strategy:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
public class Foo {
public static void main(final String[] args) throws JsonProcessingException {
final SomeObject someObject = new SomeObject();
someObject.setZxy("foobar");
final ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PascalCaseStrategy() {
#Override
public String nameForGetterMethod(final MapperConfig<?> config, final AnnotatedMethod method, final String defaultName) {
final JsonProperty annotation = method.getAnnotation(JsonProperty.class);
if (annotation != null) {
return annotation.value();
}
return super.nameForGetterMethod(config, method, defaultName);
}
});
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
System.out.println(mapper.writeValueAsString(someObject));
}
private static class SomeObject {
private String zxy;
#JsonProperty("abcDef")
public String getZxy() {
return this.zxy;
}
public void setZxy(final String zxy) {
this.zxy = zxy;
}
}
}
Output:
{"abcDef":"foobar"}

Categories