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
Nullspolicy that decides what to do when an incomingnullreaches a bean property. - The shared
Optionalcontract 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:
| Order | Knob | Default | What it omits |
|---|---|---|---|
| 1 | keepNullProperties() | off | null (and Optional.empty()) properties are omitted unless this is enabled. |
| 2 | trimStrings() | off | Trims whitespace from string values (does not omit; normalizes). |
| 3 | trimEmptyCollections() | off | Omits properties whose value is an empty Collection/array. |
| 4 | trimEmptyMaps() | off | Omits properties whose value is an empty Map. |
| 5 | nonDefault() | off | Omits 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.),
nonDefaultis 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:
| Mode | Behavior |
|---|---|
LEAVE | Pass the null straight through to the setter (default — matches historical Juneau behavior). |
EMPTY | Substitute the type's "empty" value: "" for strings, an empty mutable collection / map for collection-typed properties, Optional.empty() / OptionalInt.empty() / … for Optional properties. |
DEFAULT | Substitute the value the property holds on a freshly-constructed reference instance of the owning bean (same cached instance used by serializer nonDefault). |
SKIP | Do 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 form | Java value | Notes |
|---|---|---|
| absent property | Optional.empty() | parser fills in via ClassMeta.getOptionalDefault() |
null literal | Optional.empty() | the null is never stored bare inside the Optional |
value v | Optional.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 receivesnull(theOptionalreference itself is null). This is the historical behavior; most callers preferEMPTY.EMPTY/DEFAULT— the setter receivesOptional.empty()(or the bean's reference-instance value forDEFAULT). Never a barenullwrapped inside theOptional.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:
| Jackson | Juneau |
|---|---|
@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 coercion | Nulls.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.