Stephen Cornish
Stephen Cornish

Reputation: 21

How can I use Jackson JSON annotations to suppress empty objects?

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

Answers (1)

araqnid
araqnid

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

Related Questions