feat: allowSuppressNulls option to omit null values in serialization

IN
3d ago
+700-19

Fixes #51

What

Adds an opt-in allowSuppressNulls(boolean) flag to JsonNullableModule and JsonNullableJackson3Module:

new JsonNullableModule().allowSuppressNulls(true)

With the flag off (the default) behavior is byte-for-byte identical to today, including JsonNullable.of(null) being written as an explicit null even under NON_NULL / NON_ABSENT inclusion. With the flag on, JsonNullable-typed bean properties holding a null value (JsonNullable.of(null), ofNull(), undefined(), or a plain null reference) are omitted from the output entirely.

Why an explicit flag on the module

  • The distinction between property absent and property present with null is meaningful for PATCH-style payloads, so this cannot become the default for everyone (see the discussion in #51 — several users rely on today's semantics, and the latest suggestion there was exactly new JsonNullableModule().allowSuppressNulls(true)). The flag keeps the absent-vs-null distinction available while letting response bodies drop the bloat.
  • It deliberately does not piggyback on Jackson's Include.NON_NULL semantics: the requested output ("no JSON null for a cleared value") is not expressible with the existing JsonInclude contract for JsonNullable, since of(null) is a present-but-null reference value, not a null property.
  • Naming follows the API proposed in #51 and the module's existing fluent option style (mapBlankStringToNull(boolean)).

Where the nulls are dropped

BeanPropertyWriter writes the field name before invoking the value serializer, so the JsonNullableJackson2Serializer/JsonNullableJackson3Serializer cannot un-write a field mid-object — that is also why the serializer forces suppressNulls = false today. The suppression therefore happens in the JsonNullable bean property writers (JsonNullableJackson2BeanPropertyWriter, JsonNullableJackson3BeanPropertyWriter and both unwrapping variants), which is exactly where JsonNullable.undefined() was already omitted before this change:

  • flag off: skip only undefined() (and plain null when inclusion suppresses nulls) — unchanged;
  • flag on: additionally skip when the value is null or present-but-null (isNull()), regardless of the configured inclusion.

Consequences, documented in the README/javadoc:

  • Root values, array elements and Map values cannot skip entries mid-stream and still serialize as JSON null.
  • Non-JsonNullable properties are not affected.
  • Deserialization is untouched; a suppressed field reads back as undefined().

Tests

JsonNullableSuppressNullsTest (parameterized over Jackson 2 / Jackson 3, mirroring the existing suite):

  • flag off: default, NON_NULL and non-bean output identical to current behavior;
  • flag on: of(null) / ofNull() / plain null omitted under default, NON_NULL, NON_ABSENT, NON_EMPTY inclusion; non-null values written; NON_EMPTY content inclusion still suppresses "" unchanged;
  • flag on only affects JsonNullable properties (plain String null still written under ALWAYS);
  • root values / collection elements / Map values unchanged under the flag;
  • deserialization unaffected; write-with-flag round-trip.

Full mvn test passes (423 tests, 0 failures).


Summary by cubic

Fixes #51 by adding an opt-in allowSuppressNulls(boolean) flag to JsonNullableModule and JsonNullableJackson3Module that omits null-valued JsonNullable bean properties from serialized output.

With the flag off (the default) output is byte-for-byte identical to today: JsonNullable.of(null) still serializes as an explicit JSON null, even under NON_NULL / NON_ABSENT inclusion. With the flag on, JsonNullable bean properties holding of(null), ofNull(), undefined(), or a plain null reference are omitted entirely.

Notes

  • Suppression happens in the bean property writers (plain and unwrapping, Jackson 2 and 3), so root values, array elements, collection elements, and Map values still serialize as null.
  • Non-JsonNullable properties and deserialization are unaffected.
  • The protected (BeanPropertyWriter, PropertyName) constructors are preserved and delegate with the flag off, so existing subclasses stay source- and binary-compatible; JsonNullableBeanPropertyWriterSubclassTest guards this.
  • README default-behavior claims are scoped to bean properties.