Okay, so I've built a functional custom BeanSerializer
, which limits the depth of serialization, and I can use it as shown by creating a SerializerModifier
and adding it to module. This WORKS great and is called each time another nested field is encountered, creating the instance of DepthLimitedSerializer
perfectly. HOWEVER, when I add a custom serializer to the nested class (using @JsonSerialize
), then my modifySerializer
method NEVER RUNS on the nested field!
Here is the simple class hierarchy, of which we will serialize the outer instance of Bar
:
@Getter @Setter
public class BaseClass {
private String id;
private String someBaseProperty;
}
@Getter @Setter
//@JsonSerialize(using = FooSerializer.class)
public class Foo extends BaseClass {
private String someFooProperty;
}
@Getter @Setter
public class Bar extends BaseClass {
String someBarProperty;
Foo fooOfBar;
}
And here is the simplified custom serializer to which you pass a maxDepth
and if it reaches that depth, it ONLY serializes a single (id
) field, otherwise, it simply calls the super
:
public class DepthLimitedSerializer extends BeanSerializer {
public static int DEFAULT_DEPTH = 2;
private static final ThreadLocal<Integer> maxDepth = ThreadLocal.withInitial(() -> DEFAULT_DEPTH);
private static final ThreadLocal<Integer> currentDepth = ThreadLocal.withInitial(() -> -1);
public DepthLimitedSerializer(BeanSerializerBase src, int depth) {
super(src);
maxDepth.set(depth);
}
@Override
protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (maxDepth.get() < 0 || currentDepth.get() < maxDepth.get()) {
currentDepth.set(currentDepth.get() + 1);
super.serializeFields(bean, gen, provider);
currentDepth.set(currentDepth.get() - 1);
} else {
try {
Arrays.stream(_props).
filter(p -> p.getName().equals("id"))
.findFirst().orElseThrow()
.serializeAsField(bean, gen, provider);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
In this simple test we populate a Foo
and a Bar
and serialize them:
class SerializeTest {
@Test
void testSerialization() throws JsonProcessingException {
Foo foo = new Foo();
foo.setId("fooID");
foo.setSomeBaseProperty("someBaseValue");
foo.setSomeFooProperty("foo property value");
Bar bar = new Bar();
bar.setId("barId");
bar.setSomeBaseProperty("base of Bar");
bar.setFooOfBar(foo);
bar.setSomeBarProperty("bar property value");
String depthOfZero = testSerializationToDepthOf(bar, 0);
System.out.println("depth of ZERO: " + depthOfZero);
String depthOfOne = testSerializationToDepthOf(bar, 1);
System.out.println("depth of ONE: " + depthOfOne);
}
String testSerializationToDepthOf(BaseClass model, int depth) throws JsonProcessingException {
ObjectMapper jackson = new ObjectMapper();
jackson.enable(SerializationFeature.INDENT_OUTPUT);
SimpleModule module = new SimpleModule("TestModule");
module.setSerializerModifier(new BeanSerializerModifier() {
@Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc,
JsonSerializer<?> serializer) {
if (BaseClass.class.isAssignableFrom(beanDesc.getType().getRawClass())) {
return new DepthLimitedSerializer((BeanSerializerBase) serializer, depth);
}
return super.modifySerializer(config, beanDesc, serializer);
}
});
jackson.registerModule(module);
return jackson.writeValueAsString(model);
}
}
This gives us exactly what we expect, notice that with a depth
of 0
, we only get the id
field of the nested Foo fooOfBar
, which is correct, but at a depth
of 1
, we get the full output. This works to arbitrary depths just fine, and when it runs, that modifySerializer
method as well as those in the DepthLimitedSerializer
all run for EACH nested model!
depth of ZERO: {
"id" : "barId",
"someBaseProperty" : "base of Bar",
"someBarProperty" : "bar property value",
"fooOfBar" : {
"id" : "fooID"
}
}
depth of ONE: {
"id" : "barId",
"someBaseProperty" : "base of Bar",
"someBarProperty" : "bar property value",
"fooOfBar" : {
"id" : "fooID",
"someBaseProperty" : "someBaseValue",
"someFooProperty" : "foo property value"
}
}
HOWEVER! If one of these subclasses of BaseClass
requires custom serialization, and I attempt to add a custom serializer for a particular class, like Foo
:
public class FooSerializer extends StdSerializer<Foo> {
public FooSerializer() {
super(Foo.class);
}
public void serialize(Foo foo, JsonGenerator jgen, SerializerProvider serializerProvider)
throws IOException {
jgen.writeStartObject();
jgen.writeStringField("custom", foo.getId() + "!" + foo.getSomeFooProperty());
jgen.writeEndObject();
}
}
And I uncomment the above @JsonSerialize(using = FooSerializer.class)
line and assign the custom serializer to the class, then my modifySerializer
method and all others are NEVER RUN for my custom-annotated nested class, and so the depth
is ignored and the custom serializer just ALWAYS runs:
depth of ZERO: {
"id" : "barId",
"someBaseProperty" : "base of Bar",
"someBarProperty" : "bar property value",
"fooOfBar" : {
"custom" : "fooID!foo property value"
}
}
depth of ONE: {
"id" : "barId",
"someBaseProperty" : "base of Bar",
"someBarProperty" : "bar property value",
"fooOfBar" : {
"custom" : "fooID!foo property value"
}
}
The behavior I would have expected is for the modifySerializer
method to STILL be run, and then for the super.modifySerializer()
call to find that the FooSerializer
, but instead it's only run for the top-level object (and others, not so custom annotated).
How can I achieve this behavior? I tried to make the custom serializer extends
DepthLimitedSerializer, but they are of different types of Jackson
Serializer` and I have thus far been unable to reconcile them and get them to work together in the correct order! Clearly I can't use the annotation to assign the serializer, but how can I?
Thank you all.
Here is my final working solution. I almost think it's a bug that serializers which were assigned via annotation are NOT processed by
SerializerModifiers
attached to theObjectMapper
. Nothing in the documentation says they'll "modify serializers, but oh, not those serializers." My solution is a bit hacky, but it works.First, a custom
BeanSerializerFactory
to add running the modifiers if the serializer is found via annotation. This happens right at the beginning of the overridden method anyway.Now, this
DepthLimitedSerializer
is a LOT longer than it needs to be, mostly because I had to duplicate a lot of the code fromBeanSerailzier
. Why, you ask? Because theserialize
method inBeanSerializer
is final, so I can't override it. Instead, I'm forced to overrideBeanSerializerBase
and copy a bunch of code from it.This annotation marks the classes included and lets us pass a few arguments.
And here is our POJO hierarchy to demonstrate:
A custom serializer for
Foo
:And finally, the demonstration:
And here in the final output you can see that the custom serializer runs when the
depth
is set to1
, but doesn't run (null is used) when the depth is set to0
, exactly as it should.