Skip to content

Fix #380: Blackbird records skip stock creator for default absent components - #381

Merged
cowtowncoder merged 7 commits into
FasterXML:3.3from
phraktle:blackbird-record-absent-components
Nov 14, 2026
Merged

cowtowncoder merged 7 commits into
FasterXML:3.3from
phraktle:blackbird-record-absent-components

Conversation

@phraktle

@phraktle phraktle commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #380.

A missing component no longer forces _recordBuild when stock would give it the local's initial value: not required, no null-handling override, and an ALWAYS_NULL deserializer (or a stock scalar, for primitives). FAIL_ON_MISSING_CREATOR_PROPERTIES still takes the stock path. Other components, such as Optional, JsonNode and @JsonSetter(nulls = ...), are unchanged.

Reproducer from #378, record with one absent String: 475 ns / 1536 B → 375 ns / 1128 B, same as all components present (379 ns / 1120 B).

BBCodecRecordAbsentTest compares with stock across component kinds and the relevant features.

…bsent components

Stock gives an absent component the local's initial value (null, or zero for
a primitive) when it is not required, has no null-handling override, and its
deserializer is ALWAYS_NULL or a stock scalar. Construct directly in that case
unless FAIL_ON_MISSING_CREATOR_PROPERTIES is enabled.
@cowtowncoder cowtowncoder added blackbird Issue related to Blackbird module 3.3 labels Nov 10, 2026
@cowtowncoder cowtowncoder added the cla-received PR already covered by CLA (optional label) label Nov 10, 2026
@cowtowncoder

Copy link
Copy Markdown
Member

3.3 is the right branch -- excellent.

@stevenschlansker stevenschlansker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Opus 5.5 reviewer:

 #381: no slow path when a record component is missing (fixes #380)

 The bytecode change is fine. The bug is in how absentIsDefault() decides which missing components can use the direct path. For a non-primitive component, it only checks that getNullAccessPattern() == ALWAYS_NULL. Stock fills a missing
 component with getAbsentValue(), and that is not always null:
 - BaseScalarOptionalDeserializer, the parent of the three primitive Optional* deserializers, extends StdScalarDeserializer. It inherits ALWAYS_NULL but overrides getNullValue to return the empty Optional.
 - The probe test record Opts(String s, OptionalInt i, OptionalLong l, OptionalDouble d) read from {"s":"x"} gives this:
   - Stock: i=OptionalInt.empty, l=OptionalLong.empty, d=OptionalDouble.empty
   - #381: i=null, l=null, d=null 
 - The same happens for a generic Envelope<OptionalInt>, so the bug reaches the envelope use case behind both issues.
 - The same probe passes on #379 alone, so #381 causes it.

 Suggested fix: apply the absent shortcut only where Blackbird knows the deserializer:
 - the stock scalar kinds from STOCK_SCALAR_DESERS; 
 - CHILD codecs, whose BeanDeserializerBase reports ALWAYS_NULL and does not override the null or absent value.

 Keep Kind.STOCK on the slow path. That still covers #380's String err case.
 - Alternative: require that the deserializer overrides neither getNullValue nor getAbsentValue, checked by reflection. The allowlist above is simpler.
 - Test: add OptionalInt/OptionalLong/OptionalDouble and a converting deserializer to BBCodecRecordAbsentTest.

cowtowncoder and others added 4 commits November 13, 2026 17:24
Emit one seen-mask check for record fast-path construction, followed by
the FAIL_ON_MISSING_CREATOR_PROPERTIES check only when some component may
be absent. Add OptionalInt/OptionalLong/OptionalDouble components to
BBCodecRecordAbsentTest: their absent value is the empty Optional, which
relies on the databind#6437 fix (CONSTANT, not ALWAYS_NULL, null access).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ALWAYS_NULL describes getNullValue only; a deserializer may still return
a non-null getAbsentValue, so a STOCK component keeps the stock creator.
Allow the direct path only for the stock scalar kinds and generated
child codecs, whose absent values are known, and test a custom
deserializer with ALWAYS_NULL null access and a non-null absent value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe in the README which absent record components keep the direct
constructor call, test FAIL_ON_MISSING_CREATOR_PROPERTIES toggled per
call through ObjectReader, note that absentIsDefault relies on the exact
STOCK_SCALAR_DESERS classes, and reflow the _recordBuild comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The existing records always left a STOCK component absent alongside the
default ones, so the direct path for absent int/long/boolean and nested
codec components, and its per-call FAIL_ON_MISSING_CREATOR_PROPERTIES
check, went untested. Add a Defaults record whose constructor records
whether the stock creator called it, and assert direct construction for
documents leaving out any subset of its components.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cowtowncoder
cowtowncoder merged commit 2a65c3c into FasterXML:3.3 Nov 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.3 blackbird Issue related to Blackbird module cla-received PR already covered by CLA (optional label)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Record codegen falls back to stock creator when a component is absent

3 participants