Skip to content

Reuse the wrapped type's deserializer in value class deserializers (#1250) - #1251

Open
sandrosonntag wants to merge 1 commit into
FasterXML:3.2from
sandrosonntag:fix/value-class-delegate-deserializer-cache
Open

sandrosonntag wants to merge 1 commit into
FasterXML:3.2from
sandrosonntag:fix/value-class-delegate-deserializer-cache

Conversation

@sandrosonntag

Copy link
Copy Markdown

Fixes #1250

Problem

WrapsAnyValueClassBoxDeserializer, HasConversionCreatorWrapsSpecifiedBoxDeserializer and NoConversionCreatorBoxDeserializer call p.readValueAs(inputType) for every value. In 3.x that goes to DeserializationContext.readValue(), which looks up the root deserializer without caching it. Non-cachable deserializers (BigDecimal, UUID, java.time, ...) are therefore constructed again for every value, under the DeserializerCache lock. Details are in #1250.

Change

  • New internal WrappedValueReader: it looks up the deserializer of the wrapped type with ctxt.findRootValueDeserializer(...) on first use, keeps it in a @Volatile field and reuses it after that. Null handling and the "no current token" handling are the same as in DeserializationContext.readValue().
  • All three deserializers use it. NoConversionCreatorBoxDeserializer covers the Int/Long/String/UUID cases, which took the same path. Its subclasses now pass inputType to the constructor instead of overriding an abstract property.
  • Root semantics are unchanged: no property-level contextualization, so @JsonFormat on a value class property still does not reach the wrapped type, same as before. The lookup is still lazy, so it (and any error it reports) only happens when a value is actually read.
  • One difference: the reader uses the ctxt passed to deserialize() instead of p.objectReadContext(), like other deserializers do. With normal reading the two are the same object.

Tests

GitHub1250:

  • wrappedDeserializersAreNotConstructedPerValue counts constructions with a ValueDeserializerModifier. Without the fix the second read builds them all again (BigDecimal=40→80, LocalDate=20→40, UUID=10→20), so the test fails. With the fix nothing is built on the second read, and the test passes.
  • values covers BigDecimal, LocalDate, UUID (WrapsJavaUuid), a @JsonCreator with conversion, a nullable wrapped type, null property and list elements, and List/Map contents.
  • recursiveWrappedType covers a value class whose wrapped bean refers back to the value class.

./mvnw clean verify: 739 tests, 0 failures, 6 skipped (the same 6 are skipped on 3.2 without the change).

Measurement

I used the reproducer from #1250: 1000 items, an ObjectReader shared by N threads, items/s summed over all threads, 3 s warm-up and 5 s measurement, no JMH. "Before" is the 3.2 branch and "after" is this branch, both on databind 3.2.4. It ran on an Apple M2 Pro with JDK 21.0.5 on a busy laptop, so the absolute numbers are rough. A second run in reverse order showed the same pattern.

threads before after
1 1.22 M 1.64 M
4 0.92 M (lock-wait 53 %) 4.78 M (0 %)
12 0.85 M (lock-wait 87 %) 7.95 M (0 %)

Deserializer constructions during the second read: before {BigDecimal=1000, LocalDate=1000}, after {}.

Notes

  • Based on 3.2 because the README asks for bugfixes against the current stable version to go there. src/main is identical on 3.2 and 3.x, and I'm happy to retarget. 2.x is not affected, because readValueAs() goes through the codec's root deserializer cache there.
  • I have not touched release-notes/. I can add entries if you want them.

The value class deserializers called `p.readValueAs(inputType)` for
every value. In 3.x this ends in `DeserializationContext.readValue()`,
which looks up the root deserializer without caching it, so
non-cachable deserializers (`BigDecimal`, `UUID`, `java.time`, ...)
were constructed again for every value under the DeserializerCache lock.

Look the deserializer up once on first use and reuse it; null and
missing-token handling stays the same as in `readValue()`.

Fixes FasterXML#1250
@sandrosonntag
sandrosonntag force-pushed the fix/value-class-delegate-deserializer-cache branch from dc4b023 to 60a3076 Compare October 27, 2026 05:14
@sandrosonntag

Copy link
Copy Markdown
Author

I have sent the signed CLA (CLA-jackson-2026) to cla@fasterxml.com today.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant