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;
}
}