Jackson custom serialization and deserialization - java

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());

Related

Jackson how map one Pojo field to 2 (json) fields (same content, different name)

I use Jackson to serialise POJOs into CSV. Now we need to change the naming for certain fields to snake_case. This is easily done by #JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class).
For compatibility reasons we need some of the renamed fields also with their old name.
E.g.:
public class Pojo {
private int someField;
}
Default will serialise to "someField", SnakeCaseStrategy will serialise to "some_field".
How to get serialization with both?:
{
"someField" : "one",
"some_field" : "one"
}
My first try was a mixin:
public abstract class PojoFormat {
#JsonProperty("someField")
abstract String getSomeField();
}
but this effectively only undoes the naming strategy change.
So how to copy a field in serialization - preferable not by changing the Pojo (this copied fields should be removed when all clients can cope with it).
Little update:
in my real class there some nested class that use JsonUnwrapped and the doc stated that this is not working with custom serializer (didn't know that this makes a difference here).
Well, I have never seen this before, I would be very happy if someone here in this site knows how.
The easy way, in my opinion, is to use a Custom Serializer.
For example:
Using the #JsonSerialize annotation
Register a module
Dynamic Serializer with Reflection
#JsonSerialize annotation
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
#JsonSerializer(using=PojoSerializer.class)
class Pojo {
private String myValue;
// getters and setters
}
class PojoSerializer extends StdSerializer<Pojo> {
public PojoSerializer() {
super(Pojo.class);
}
#Override
public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("myValue", value.getMyValue());
gen.writeStringField("my_value", value.getMyValue());
gen.writeEndObject();
}
}
Module
static class Pojo {
private String myValue;
public String getMyValue() {
return myValue;
}
public Pojo setMyValue(String myValue) {
this.myValue = myValue;
return this;
}
}
static class PojoSerializer extends StdSerializer<Pojo> {
public PojoSerializer() {
super(Pojo.class);
}
#Override
public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("myValue", value.getMyValue());
gen.writeStringField("my_value", value.getMyValue());
gen.writeEndObject();
}
}
public static void main(String[] args) throws JsonProcessingException {
final ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule("PojoModule");
module.addSerializer(Pojo.class, new PojoSerializer());
mapper.registerModule(module);
final Pojo pojo = new Pojo();
pojo.setMyValue("This is the value of my pojo");
System.out.println(mapper.writeValueAsString(pojo));
}
Reflection
I write some code for you, you might want to see to get new ideias.
This works as a generic way(just to not write several serializers).
// The serializer will be register in the ObjectMapper module.
static class Pojo {
private String myValue = "With snake and camel";
private String value = "Without snake case";
private String thirdValue = "snake & camel";
}
// using the annotation
#JsonSerialize(using = PojoSerializer.class)
static class Pojo2 {
private String pojoName = "Pojo 2";
private String pojo = "pojp";
}
static class PojoSerializer extends StdSerializer<Object> {
public PojoSerializer() {
super(Object.class);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
final Field[] fields = value.getClass().getDeclaredFields();
for(final Field field : fields) {
final String name = field.getName();
final String fieldValue;
try {
// Do not use this!
fieldValue = (String)field.get(value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
byte firstUpperCase = -1;
for(byte index = 0; index < name.length(); index++) {
final char caractere = name.charAt(index);
// A ascii code is 66 decimal, and 90 is the Z in decimal
if(caractere > 'A' && caractere < 'Z') {
// found the first upper
firstUpperCase = index;
break;
}
}
// writes the normal field name
gen.writeStringField(name, fieldValue);
// if the name is in camel case, we will write in snake case too.
if(firstUpperCase != -1) {
final char lowerLetter = (char)((int) name.charAt(firstUpperCase) + 32);
final String left = name.substring(0, firstUpperCase);
final String right = String.format("%c%s",lowerLetter, name.substring(firstUpperCase + 1));
gen.writeStringField(String.format("%s_%s", left, right), fieldValue);
}
}
gen.writeEndObject();
}
}
You can try to use JsonAnyGetter annotation and define for every POJO extra mapping for backward compatibility.
Let's create a simple interface:
interface CompatibleToVer1 {
#JsonAnyGetter
Map<String, Object> getCompatibilityView();
}
and two classes which implement it:
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class RootPojo implements CompatibleToVer1 {
private int rootId;
#JsonUnwrapped
private SomePojo pojo;
#Override
public Map<String, Object> getCompatibilityView() {
return Collections.singletonMap("rootId", rootId);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class SomePojo implements CompatibleToVer1 {
private int someField;
private String someName;
#Override
public Map<String, Object> getCompatibilityView() {
Map<String, Object> extra = new LinkedHashMap<>();
extra.put("someField", someField);
return extra;
}
}
As you can see, I defined extra columns for each POJO with custom names. Serialising to JSON is straightforward:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
SomePojo pojo = new SomePojo(123, "Tom");
mapper.writeValue(System.out, new RootPojo(1, pojo));
Above code prints:
{
"root_id" : 1,
"some_field" : 123,
"some_name" : "Tom",
"someField" : 123,
"rootId" : 1
}
But for CSV we need to create extra configuration:
CsvMapper csvMapper = CsvMapper.builder().build();
CsvSchema pojoExtraScheme = CsvSchema.builder()
.addColumn("someField")
.build();
CsvSchema rootExtraScheme = CsvSchema.builder()
.addColumn("rootId")
.build();
CsvSchema compatibleSchema = CsvSchema.emptySchema()
.withHeader()
.withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
.withColumnsFrom(rootExtraScheme)
.withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
.withColumnsFrom(pojoExtraScheme);
SomePojo tom = new SomePojo(123, "Tom");
SomePojo jerry = new SomePojo(124, "Jerry");
List<RootPojo> pojos = Arrays.asList(new RootPojo(1, tom), new RootPojo(2, jerry));
ObjectWriter writer = csvMapper.writer(compatibleSchema);
System.out.println(writer.writeValueAsString(pojos));
Above code prints:
some_field,some_name,root_id,rootId,someField
123,Tom,1,1,123
124,Jerry,2,2,124
If you do not want to specify extra columns two times you can implement builder method based on our interface:
CsvSchema createSchemaFor(CompatibleToVer1 entity) {
CsvSchema.Builder builder = CsvSchema.builder();
entity.getCompatibilityView().keySet().forEach(builder::addColumn);
return builder.build();
}
and use as below:
CsvSchema compatibleSchema = CsvSchema.emptySchema()
.withHeader()
.withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
.withColumnsFrom(createSchemaFor(new RootPojo()))
.withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
.withColumnsFrom(createSchemaFor(new SomePojo()));
Using JsonAnyGetter with CSV is really tricky and could be problematic mixing it with other annotations, take a look at: Could please add JsonAnyGetter and JsonAnySetter annotations support?

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.

How to serialise Enums as both Object Shape and default string?

For an enum with attributes, eg:
public enum Thing {
THING_A("a"),
THING_B("b");
private String thing;
private Thing(String thing) {
this.thing = thing;
}
// Getters...
}
Jackson serializes as the name of the values, eg:
mapper.writeValueAsString(Thing.THING_A)); // "THING_A"
If we add the annotation to treat serialisation as an object:
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
it will serialize the attributes:
mapper.writeValueAsString(Thing.THING_A)); // "{"thing":"a"}"
I'd like to be able to decide, during serialization, which of these methods to use. Because this spans a large number of enums, I'd rather not edit each one. Is there a good way to do this?
eg: something like this would be great:
mapper.writeValueAsString(Thing.THING_A, JsonFormat.Shape.OBJECT); // "{"thing":"a"}"
mapper.writeValueAsString(Thing.THING_A, JsonFormat.Enum.DEFAULT); // "THING_A"
Since, com.fasterxml.jackson.annotation.JsonFormat is an annotation you can implement your own com.fasterxml.jackson.databind.AnnotationIntrospector and return value you want for all your enums. Simple example you can find below:
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
public class JsonPathApp {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(AnnotationIntrospector.pair(new DynamicEnumAnnotationIntrospector(), new JacksonAnnotationIntrospector()));
System.out.println(mapper.writeValueAsString(Thing.THING_A));
}
}
class DynamicEnumAnnotationIntrospector extends AnnotationIntrospector {
#Override
public Version version() {
return new Version(1, 0, 0, "Dynamic enum object", "your.package", "jackson.dynamic.enum");
}
#Override
public JsonFormat.Value findFormat(Annotated memberOrClass) {
final Class<?> rawType = memberOrClass.getRawType();
if (rawType.isEnum() && rawType.getPackage().getName().startsWith("your.package")) {
return JsonFormat.Value.forShape(JsonFormat.Shape.OBJECT);
}
return super.findFormat(memberOrClass);
}
}
Above code prints:
{"thing":"a"}
Now, you can create two instances of ObjectMapper and for one configure your own annotation introspector and second one leave with default. If you really want to use it in dynamic way you can create one ObjectMapper for each available Shape value and select required one for a given shape:
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Objects;
public class JsonPathApp {
public static void main(String[] args) throws Exception {
JsonFactory factory = new JsonFactory();
for (Shape shape : Shape.values()) {
ObjectMapper mapper = factory.getWithEnumShapeSetTo(shape);
System.out.println(shape + " => " + mapper.writeValueAsString(Thing.THING_A));
}
}
}
class JsonFactory {
private final AnnotationIntrospector defaultIntrospector = new JacksonAnnotationIntrospector();
private final EnumMap<Shape, ObjectMapper> instances = new EnumMap<>(Shape.class);
public JsonFactory() {
final List<Shape> notAllowed = Arrays.asList(Shape.BOOLEAN, Shape.BINARY);
Arrays.stream(Shape.values())
.filter(shape -> !notAllowed.contains(shape))
.forEach(shape -> instances.put(shape, createNewWithEnumShape(shape)));
}
private ObjectMapper createNewWithEnumShape(Shape shape) {
DynamicEnumAnnotationIntrospector enumIntrospector = new DynamicEnumAnnotationIntrospector(shape);
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(AnnotationIntrospector.pair(enumIntrospector, defaultIntrospector));
return mapper;
}
public ObjectMapper getWithEnumShapeSetTo(Shape shape) {
Objects.requireNonNull(shape);
final ObjectMapper mapper = instances.get(shape);
if (mapper == null) {
return new ObjectMapper();
}
return mapper;
}
}
class DynamicEnumAnnotationIntrospector extends AnnotationIntrospector {
private final Shape shape;
public DynamicEnumAnnotationIntrospector(Shape shape) {
this.shape = Objects.requireNonNull(shape);
}
#Override
public Version version() {
return new Version(1, 0, 0, "Dynamic enum shape", "your.package", "jackson.dynamic.enum");
}
#Override
public JsonFormat.Value findFormat(Annotated memberOrClass) {
final Class<?> rawType = memberOrClass.getRawType();
if (rawType.isEnum() && rawType.getPackage().getName().startsWith("your.package")) {
return JsonFormat.Value.forShape(shape);
}
return super.findFormat(memberOrClass);
}
}
Above code prints:
ANY => "THING_A"
NATURAL => "THING_A"
SCALAR => "THING_A"
ARRAY => 0
OBJECT => {"thing":"a"}
NUMBER => 0
NUMBER_FLOAT => 0
NUMBER_INT => 0
STRING => "THING_A"
BOOLEAN => "THING_A"
BINARY => "THING_A"
Above code of course is overkill but I wanted to show possibilities we have. We have only 3 different outputs so you can group values with the same output and create maximum 3 different ObjectMappers.
The above question is similar and has already been answered.
Jackson ObjectMapper set JsonFormat.Shape.ARRAY without annotation.
You can use custom object mapper specific for the Enum and different object mapper for other classes.

Mask json fields using jackson

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.

Categories