Skip to content

Blackbird: use generated setters for builder-based deserialization - #364

Merged
cowtowncoder merged 35 commits into
FasterXML:2.xfrom
parker89:pw/blackbird-builder-setters
Oct 30, 2026
Merged

cowtowncoder merged 35 commits into
FasterXML:2.xfrom
parker89:pw/blackbird-builder-setters

Conversation

@parker89

@parker89 parker89 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Builder deserialization currently goes through MethodProperty, even though Blackbird already creates an accessor that returns the setter's result. Keep that accessor and use it from setAndReturn and deserializeSetAndReturn.

Second in the stack: #366 → #364 → #367. #366 is merged, so this diff now has only the builder change.

A setter's non-null result becomes the next builder; a null result keeps the current builder. Primitive int, long and boolean arguments avoid boxing. Void setters, incompatible values or receivers, and setters Jackson leaves subject to normal access checks continue through reflection. Builder properties that databind assigns through set() (buffered before a property-based creator, or external type ids) are handled the same way, so the outcome doesn't depend on the order of properties in the JSON. Null handling and setter error reporting match MethodProperty, including JSON location. (Builder setter failures already lacked the location before this PR, since MethodProperty.setAndReturn has no parser either.)

Once the generated setter is called, a failure is never retried, so a setter never runs twice. Telling a failure of the generated accessor apart from the setter's own would mean guessing from the stack trace, so both are reported like a setter failure. #366 prevents the known accessor failures at setup. The trade-off is that an accessor failure those checks miss now fails deserialization instead of falling back to reflection. #367 does the same for regular bean setters.

Tests verify that mutable and copying builders use generated setters, compare edge cases and failures with vanilla Jackson, and check that real JVM access failures are reported without running the setter. All Blackbird tests pass on Java 8, 11, 17 and 25.

The new builderArrayDeser benchmark deserializes 1,000 beans per operation. On JDK 25, allocation fell from 249.5 to 209.5 KB/op, mostly from avoiding primitive boxing. These measurements predate the review changes that added receiver/type checks. Throughput varied too much on my laptop to quote a reliable improvement.

Benchmark commands (use a disposable checkout)
# Run from a clean checkout of this branch. The benchmarks POM is outside the
# reactor and its parent is pinned to 2.19.0, so point it at the snapshot that
# each step installs, and build the benchmarks from this branch both times.
set -euo pipefail
BASE=644e951f      # merge base with 2.x
HEAD=$(git rev-parse HEAD)
JMH_ARGS=(-f 2 -wi 5 -w 2s -i 5 -r 3s -prof gc)

build() {  # $1 = commit to install Blackbird from, $2 = output jar
  git checkout --no-overlay -q "$1" -- blackbird afterburner
  ./mvnw -B -q -ntp clean install -pl blackbird,afterburner -am -DskipTests
  git checkout --no-overlay -q "$HEAD" -- blackbird afterburner
  (cd blackbird/benchmarks \
    && sed -i.bak 's|<version>2.19.0</version>|<version>2.23.0-SNAPSHOT</version>|' pom.xml \
    && ../../mvnw -B -q -ntp clean package -DskipTests \
    && mv pom.xml.bak pom.xml)
  cp blackbird/benchmarks/target/benchmarks.jar "$2"
}

build "$BASE" /tmp/benchmarks-base.jar
build "$HEAD" /tmp/benchmarks-head.jar
java -jar /tmp/benchmarks-base.jar '\.Blackbird\.builderArrayDeser$' "${JMH_ARGS[@]}"
java -jar /tmp/benchmarks-head.jar '\.Blackbird\.builderArrayDeser$' "${JMH_ARGS[@]}"

CLA submitted by email.

@parker89
parker89 marked this pull request as ready for review October 26, 2026 18:10
@cowtowncoder cowtowncoder added the cla-received PR already covered by CLA (optional label) label Oct 28, 2026
@cowtowncoder

Copy link
Copy Markdown
Member

@stevenschlansker WDYT? Obviously only matters pre-rewrite but could merge in 2.x / merge forward to 3.1, 3.2, if this makes sense.

@cowtowncoder cowtowncoder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A few issues found while reviewing; details inline.

Claude Code on behalf of @cowtowncoder

@cowtowncoder cowtowncoder added the blackbird Issue related to Blackbird module label Oct 29, 2026
…/blackbird-builder-setters

# Conflicts:
#	blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java
#	blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java
#	blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java
#	blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java
@parker89

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Fixing it grew the change quite a bit, so I split it into three stacked PRs, to be merged in order:

  1. Blackbird: skip generated setters the accessor can't call #366: setup checks so Blackbird skips generated setters it can't call. These also fix existing problems with regular beans, so it stands on its own.
  2. This PR: generated setters for builders.
  3. Blackbird: avoid retrying failed bean setters #367: no more retries of failed regular setters, plus Blackbird ignores @JsonSetter(nulls = Nulls.SKIP) for properties of beans with a properties-based creator #363.

Each PR's diff includes the earlier ones until they're merged, so #366 is the place to start.

@cowtowncoder

Copy link
Copy Markdown
Member

+1 for splitting into stacked PRs! I'll need to weave changes wrt #359 that goes in 3.x so this might slightly simplify that too.

@cowtowncoder cowtowncoder added this to the 2.23.0 milestone Oct 30, 2026
cowtowncoder and others added 3 commits October 29, 2026 20:21
2.x has FasterXML#366 squashed; this branch already had it up to 2806ae1, so
resolve against that rather than the older merge base. Keep both release
notes entries and both sets of tests; builder tests now use
`_mapperWithLookupIn`, and expect inherited protected builder setters to
be optimized on JDK 14+ as for beans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
databind also calls set() on builders, for properties buffered before a
property-based creator and for external type ids, so whether a setter
failure was retried, how it was reported, and whether access modifiers
were honored depended on the order of properties in the JSON.

- Builder properties now report failures from set() like setAndReturn():
  no retry through reflection, wrapped as MethodProperty does
- A builder setter Jackson wouldn't make accessible is left to reflection
  entirely, not just for setAndReturn()
- Primitive setAndReturn() checks for a generated setter before unboxing

Regular beans keep their existing behavior (see TestAccessFallback).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Treat a builder without a build method (`buildMethodName=""`) as a
  builder too, by also checking for its builder config
- Leave a builder setter to reflection only if reflection itself couldn't
  call it: a public setter of a public class in an exported package stays
  optimized even with OVERRIDE_PUBLIC_ACCESS_MODIFIERS disabled
- Keep calling the 3-arg findOptimizableProperties() for regular beans, so
  subclasses overriding it still work
- set() skips nulls for Nulls.SKIP as MethodProperty does, and primitive
  set() leaves a null to the delegate instead of throwing a raw NPE

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@parker89

parker89 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Sounds good, thanks for merging #366 . I'll hold off on any further changes to this PR or #367 until you give me the go ahead

cowtowncoder and others added 4 commits October 30, 2026 10:12
…ible

databind makes some setters accessible before Blackbird sees them (for
injection or back references). `unreflect` then skips its access checks,
so a package-private setter inherited from another package passed, and
creating the generated accessor failed the whole deserializer. Unreflect
a fresh copy instead, so such setters are left to reflection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- isMadeAccessible() has the property fix its access, as databind does
  when building the deserializer, instead of re-implementing its rules
- Non-void setters adapt their generated thunk with a plain lambda rather
  than spinning a trampoline class per property (about 30-40% faster
  deserializer construction for a 20-setter builder)
- Pass the setter's value type to SettableObjectProperty, and move
  _reflectiveSetAndReturn there as a private helper
- Drop the unused value argument of _reportProblemAndReturn
- Remove tests for the stack-trace retry heuristic, which no longer exists
- Benchmarks: use the current parent version, so they measure this code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A void setter's generated consumer is adapted to a thunk returning null,
so setAndReturn() keeps the same builder (as MethodProperty does) instead
of going through reflection; a missing thunk now only means the setter is
left to reflection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set() only calls the generated setter with a value of the setter's own
type, on an instance it can take; anything else goes through reflection,
so it fails (or is widened) as without Blackbird, instead of throwing a
ClassCastException. This also stops int/long setters from silently
truncating another Number type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cowtowncoder and others added 2 commits October 30, 2026 11:26
- Only builder properties get the setAndReturn thunk and the access gate
  (with its early fixAccess); regular beans leave setAndReturn to
  reflection, as before this branch
- set() only checks the instance type for builders, keeping that check
  off the hot path of regular beans

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… error

The warning claimed Blackbird deserialization was disabled for the
property, but the generated setter keeps being used and falls back to
reflection on each such error; only the warning is logged once. Reword
it, and rename the flag that only guards the logging. No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cowtowncoder
cowtowncoder merged commit 476b7e1 into FasterXML:2.x Oct 30, 2026
4 checks passed
@cowtowncoder cowtowncoder changed the title Blackbird: use generated setters for builder-based deserialization (2.x) Blackbird: use generated setters for builder-based deserialization Oct 30, 2026
cowtowncoder added a commit to parker89/jackson-modules-base that referenced this pull request Oct 30, 2026
Keeps 2.x's squashed builder-setter changes (FasterXML#364) and re-applies this
branch's no-retry and null-handling changes on top. TestSetterFailures is
updated for 2.x's SettableObjectProperty constructor and removal of
BBDeserializerModifier.trampoline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cowtowncoder added a commit that referenced this pull request Oct 30, 2026
Ports #364 (Blackbird: use generated setters for Builder-based
deserialization) to 3.x, following 3.x `MethodProperty`, which calls
setters through a method handle from databind's own module:

- Setter failures are reported with `_throwAsJacksonE(p, e, value)` as is
  (no `InvocationTargetException` wrapping; Errors are rethrown as they are)
- Values or builders the generated setter can't take are left to the
  delegate, which fails as without Blackbird
- A public builder setter Jackson didn't make accessible is only optimized
  if databind's module reads its module and has its package exported to it

Tests adapted accordingly; the `TextNode(null)` test is dropped, as
`StringNode` rejects null in 3.x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cowtowncoder added a commit to stevenschlansker/jackson-modules-base that referenced this pull request Oct 30, 2026
Brings in the 2.x port of generated builder setters (FasterXML#364) for the old
LambdaMetafactory engine. This branch's generated-codec engine already
handles builders, so its BBDeserializerModifier is kept and the
Settable*Property classes stay deleted. The new builder tests are
adapted to it: they check for a generated reader instead of optimized
properties, drop the cases that construct the removed property classes,
and expect the List property (a stock arm) to go through reflection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cowtowncoder added a commit that referenced this pull request Oct 30, 2026
Brings #367 (Blackbird: don't retry failed bean setters through
reflection), and adds a 3.2.5 release notes section listing the
Blackbird changes from 3.1.9 (#363, #364, #366, #367).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

2 participants