Reputation: 21
I am attempting to serialise an object similar to the following:
class Header {
public String title;
public String author;
}
class Document {
public String data;
public Header header = new Header();
}
Without any annotations, jackson will serialise to:
{"data":null,"header":{"title":null,"author":null}}
Using @JsonInclude(NOT_NULL)
or @JsonInclude(NOT_EMPTY)
on Header
and Document
, it will serialise to:
{"header":{}}
I want to suppress the header
property if it would be empty but this only supported for collections and strings. Ideally this would be solved via an annotation as the default BeanSerializer
would be able to achieve this easily.
I can achieve what I need by writing a custom serializer but then I lose the benefit of all the advanced logic inside the default serializers.
Can anyone think of a better way to solve this? The structures above are an example only as I'm looking for a generic solution.
Upvotes: 1
Views: 589
Reputation: 133722
It's not really all that easy- basically, the bean serializer would have to hold off on writing the field name for a property until something was written by a property serializer-- this could get particularly hairy once you have several such nested within each other.
The easiest way to solve it would be for the property serializer to serialize its value to a token buffer, and then skip the property if the token buffer only contained "{" and "}". But this means that a graph of objects would end up being read in and out of buffers at each level, which defeats the purpose of having a streaming generator that doesn't build up content proportional to the size of the generated output.
If you can live with that buffer copying, this will do approximately what you ask:
@Test
public void suppress_empty_objects() throws Exception {
mapper.registerModule(new SimpleModule() {
@Override
public void setupModule(SetupContext context) {
context.addBeanSerializerModifier(new SuppressEmptyBean());
}
class SuppressEmptyBean extends BeanSerializerModifier {
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
// TODO: examine bean description for annotations to enable/disable suppression
ListIterator<BeanPropertyWriter> iter = beanProperties.listIterator();
while (iter.hasNext()) {
BeanPropertyWriter beanPropertyWriter = iter.next();
// TODO: only relevant to suppress properties that are themselves beans
iter.set(new SuppressEmptyPropertyWriter(beanPropertyWriter));
}
return beanProperties;
}
}
class SuppressEmptyPropertyWriter extends BeanPropertyWriter {
private final BeanPropertyWriter underlying;
SuppressEmptyPropertyWriter(BeanPropertyWriter underlying) {
super(underlying);
this.underlying = underlying;
}
@Override
public void serializeAsField(Object bean, JsonGenerator output, SerializerProvider prov)
throws Exception {
TokenBuffer tokenBuffer = new TokenBuffer(output.getCodec(), false);
underlying.serializeAsField(bean, tokenBuffer, prov);
if (!suppress(tokenBuffer, output)) {
tokenBuffer.serialize(output);
}
}
private boolean suppress(TokenBuffer tokenBuffer, JsonGenerator output) throws JsonParseException,
IOException {
if (tokenBuffer.firstToken() != JsonToken.FIELD_NAME) return false; // nope
JsonParser bufferParser = tokenBuffer.asParser();
bufferParser.nextToken(); // on field name
JsonToken valueToken1 = bufferParser.nextToken(); // on start object
if (valueToken1 != JsonToken.START_OBJECT) return false;
JsonToken valueToken2 = bufferParser.nextToken(); // on first thing inside object
return valueToken2 == JsonToken.END_OBJECT;
}
}
});
Document document = new Document();
document.data = "test";
assertThat(mapper.writeValueAsString(document), equivalentTo("{ data: 'test' }"));
document.header = new Header();
assertThat(mapper.writeValueAsString(document), equivalentTo("{ data: 'test' }"));
document.header.title = "the title";
assertThat(mapper.writeValueAsString(document),
equivalentTo("{ data: 'test', header: { title: 'the title' } }"));
}
Upvotes: 1