Skip to main content

Null & Inclusion Policies

Juneau gives you fine-grained control over what gets emitted on the wire and what happens when a null arrives during parsing. The two surfaces are co-designed so the round-trip behavior is predictable across every format (JSON, JSON5, XML, HTML, YAML, MsgPack, …).

This page documents:

  • The serializer inclusion knobs that suppress empty / default values.
  • The parser Nulls policy that decides what to do when an incoming null reaches a bean property.
  • The shared Optional contract that both sides honor.
  • A short Jackson @JsonInclude / @JsonSetter(nulls=…) mapping table.

Serializer inclusion knobs​

All inclusion knobs live on Serializer.Builder and apply uniformly across every format. They are evaluated in a fixed precedence order — the first one that decides "omit" wins:

OrderKnobDefaultWhat it omits
1keepNullProperties()offnull (and Optional.empty()) properties are omitted unless this is enabled.
2trimStrings()offTrims whitespace from string values (does not omit; normalizes).
3trimEmptyCollections()offOmits properties whose value is an empty Collection/array.
4trimEmptyMaps()offOmits properties whose value is an empty Map.
5nonDefault()offOmits properties whose value equals the type's default.

Note: null is governed by keepNullProperties, not by nonDefault. The two knobs compose — see below.

nonDefault()​

The nonDefault() knob suppresses any property whose value is equal to its default. "Default" means:

  • For primitives and boxed wrappers — the Java type default (0, 0L, 0.0, false, '\u0000', null).
  • For bean properties — the value the property holds on a freshly-constructed instance of the owning bean via its no-arg constructor. The reference instance is built once and cached on the bean's ClassMeta.
  • If a bean's no-arg constructor cannot be invoked (private, throws, etc.), nonDefault is silently skipped for that bean and properties are emitted normally. The knob never aborts a serialization.

Equality is by value, not by reference. Numeric comparisons use a BigDecimal.compareTo-style check so 1 ≡ 1.0, 0.0 ≡ -0.0, 0 ≡ 0L, and so on. For everything else, Objects.equals decides.

public class Person {
public String name = "Anonymous";
public int age = 0;
public boolean active;
}

var s = JsonSerializer.create().nonDefault().build();

var p = new Person();
s.write(p); // → {}
p.name = "Alice";
s.write(p); // → {"name":"Alice"}
p.age = 21;
s.write(p); // → {"name":"Alice","age":21}

Composing with keepNullProperties()​

Because nonDefault does not govern null, the two knobs compose cleanly:

public class Bean {
public Integer boxed; // default = null
public String s; // default = null
public int i; // default = 0
public double d; // default = 0.0
}

var s = JsonSerializer.create().nonDefault().keepNullProperties().build();
s.write(new Bean()); // → {"boxed":null,"s":null}

The primitive defaults i:0 / d:0.0 are omitted by nonDefault; the explicit null properties are kept by keepNullProperties.

Configuring via annotation​

The same setting is available on @SerializerConfig:

@SerializerConfig(nonDefault = "true")
@Rest(serializers = JsonSerializer.class)
public class MyResource extends BasicRestServlet {}

Parser null-coercion: the Nulls policy​

When a JSON null (or its equivalent in any format) reaches a bean property during parsing, you can choose what value the setter actually receives via the Nulls enum:

ModeBehavior
LEAVEPass the null straight through to the setter (default — matches historical Juneau behavior).
EMPTYSubstitute the type's "empty" value: "" for strings, an empty mutable collection / map for collection-typed properties, Optional.empty() / OptionalInt.empty() / … for Optional properties.
DEFAULTSubstitute the value the property holds on a freshly-constructed reference instance of the owning bean (same cached instance used by serializer nonDefault).
SKIPDo not invoke the setter at all — any pre-existing field initializer or earlier assignment is preserved.

The policy can be set at three levels (per-property wins, then session, then context):

public class Bean {
@MarshalledProp(nulls = Nulls.EMPTY)
public String s;

@MarshalledProp(nulls = Nulls.SKIP)
public String t = "initial";

public String u;
}

var p = JsonParser.create().nulls(Nulls.LEAVE).build();
var b = p.read("{\"s\":null,\"t\":null,\"u\":null}", Bean.class);
// b.s == "" (per-prop EMPTY wins)
// b.t == "initial" (per-prop SKIP preserves initializer)
// b.u == null (no per-prop override; falls through to context LEAVE)

Configuring via annotation​

The context-level default is also configurable on @ParserConfig:

@ParserConfig(nulls = "EMPTY")
@Rest(parsers = JsonParser.class)
public class MyResource extends BasicRestServlet {}

Shared Optional contract​

Optional<T>, OptionalInt, OptionalLong, and OptionalDouble participate in both the inclusion and the null-coercion contracts as first-class types:

Wire formJava valueNotes
absent propertyOptional.empty()parser fills in via ClassMeta.getOptionalDefault()
null literalOptional.empty()the null is never stored bare inside the Optional
value vOptional.of(v)parser unwraps and re-wraps automatically

On serialization, Optional.empty() (and OptionalInt.empty() / OptionalLong.empty() / OptionalDouble.empty()) is treated identically to null: omitted unless keepNullProperties() is set, in which case it is emitted as null. A present Optional is emitted as its unwrapped value.

On per-property null-coercion of an Optional property:

  • LEAVE — the setter receives null (the Optional reference itself is null). This is the historical behavior; most callers prefer EMPTY.
  • EMPTY / DEFAULT — the setter receives Optional.empty() (or the bean's reference-instance value for DEFAULT). Never a bare null wrapped inside the Optional.
  • SKIP — the setter is not invoked; the field's initializer value (or earlier assignment) survives.

Jackson mapping​

For migrators coming from Jackson, the rough equivalents are:

JacksonJuneau
@JsonInclude(NON_NULL) (default on most setups)default behavior (keepNullProperties is off)
@JsonInclude(NON_EMPTY)trimEmptyCollections() + trimEmptyMaps() (+ trimStrings() for whitespace-only)
@JsonInclude(NON_DEFAULT)nonDefault()
@JsonSetter(nulls = Nulls.AS_EMPTY)@MarshalledProp(nulls = Nulls.EMPTY)
@JsonSetter(nulls = Nulls.SKIP)@MarshalledProp(nulls = Nulls.SKIP)
@JsonSetter(nulls = Nulls.SET) (Jackson default)Nulls.LEAVE (Juneau default)
DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL + setter coercionNulls.DEFAULT on the offending property

The Juneau knobs are intentionally at the model level (Serializer.Builder / Parser.Builder / @MarshalledProp), so the behavior is identical across every format — there is no format-specific opt-in.


See also​