37 runnable examples covering the eight feature posts on ankurm.com, verified against Jackson 3.2.1 on Temurin 21.0.5. Every output committed under docs/ was produced by run-all.sh. Also documents 11 places where the published snippets do not compile or do not behave as printed against a real Jackson 3 build - most notably that writeValueAsString(List<Base>) silently drops the polymorphic type discriminator, so the post's serialised output cannot be read back.
150 lines
6.4 KiB
Markdown
150 lines
6.4 KiB
Markdown
# Beyond the posts
|
|
|
|
Behaviour that no post in the series covers, but that shows up on the first real
|
|
upgrade. Every claim here is printed by a program rather than asserted.
|
|
|
|
## Y01 — unchecked exceptions
|
|
|
|
[`Y01UncheckedExceptions.java`](../src/main/java/com/ankurm/jackson3/beyond/Y01UncheckedExceptions.java)
|
|
|
|
`JacksonException extends RuntimeException`. The comparison post is right that this is
|
|
the most dangerous change, but the failure mode is more specific than "your catch blocks
|
|
stop working". There are two cases:
|
|
|
|
- **The try block contains only Jackson calls.** `catch (IOException)` becomes a
|
|
*compile error* — "exception java.io.IOException is never thrown in body of
|
|
corresponding try statement". The compiler saves you.
|
|
- **The try block also does real I/O.** `IOException` is still reachable, so the catch
|
|
block compiles and simply stops covering the Jackson call.
|
|
|
|
The second is the common shape in real code — read a request body, parse it — and it is
|
|
what runs below.
|
|
|
|
```
|
|
JacksonException extends RuntimeException : true
|
|
JacksonException extends IOException : false
|
|
|
|
-- catch (IOException) around I/O + Jackson --
|
|
ESCAPED the IOException handler -> StreamReadException
|
|
-- catch (JacksonException) then catch (IOException) --
|
|
caught: StreamReadException
|
|
-- unchecked exceptions inside a stream --
|
|
[{"orderId":1,"customerName":"Alice"}, {"orderId":2,"customerName":"Bob"}]
|
|
```
|
|
|
|
The upside is real too: Jackson calls now compose inside lambdas and streams without a
|
|
checked-exception wrapper, as the last line shows.
|
|
|
|
## Y02 — FAIL_ON_TRAILING_TOKENS
|
|
|
|
[`Y02TrailingTokens.java`](../src/main/java/com/ankurm/jackson3/beyond/Y02TrailingTokens.java)
|
|
|
|
Off in Jackson 2, on in Jackson 3. Concatenated or double-encoded JSON that used to
|
|
parse — reading the first document and discarding the rest — now throws.
|
|
|
|
```
|
|
FAIL_ON_TRAILING_TOKENS default : true
|
|
|
|
Jackson 3 default -> rejected: MismatchedInputException
|
|
2.x behaviour -> accepted: OrderDto[orderId=1]
|
|
garbage, default -> rejected: StreamReadException
|
|
garbage, relaxed -> accepted: OrderDto[orderId=1]
|
|
```
|
|
|
|
A correctness improvement, but it surfaces as new runtime failures on payloads that
|
|
previously "worked", which is a bad thing to discover in production.
|
|
|
|
## Y03 — where WRITE_DATES_AS_TIMESTAMPS went
|
|
|
|
[`Y03DateTimeDefaults.java`](../src/main/java/com/ankurm/jackson3/beyond/Y03DateTimeDefaults.java)
|
|
|
|
Several blog snippets carry `.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)`
|
|
over from Jackson 2. That constant is not on `SerializationFeature` in Jackson 3, so the
|
|
code does not compile. It moved to `tools.jackson.databind.cfg.DateTimeFeature` and
|
|
defaults to off, so there is nothing to disable.
|
|
|
|
```
|
|
SerializationFeature has WRITE_DATES_AS_TIMESTAMPS : false
|
|
DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS default : false
|
|
|
|
defaults (ISO-8601):
|
|
{"day":"2026-09-15","startsAt":"2026-09-15T10:30:00","recordedAt":"2026-09-15T10:30:00Z","zoned":"2026-09-15T10:30:00Z","length":"PT45M"}
|
|
with WRITE_DATES_AS_TIMESTAMPS enabled:
|
|
{"day":[2026,9,15],"startsAt":[2026,9,15,10,30],"recordedAt":1789468200.000000000,"zoned":1789468200.000000000,"length":"PT45M"}
|
|
numeric form reads back: 2026-09-15
|
|
```
|
|
|
|
Both forms read back, so stored payloads written by an older service still parse.
|
|
|
|
## Y04 — configuring an immutable mapper
|
|
|
|
[`Y04ImmutableMapperAndReaders.java`](../src/main/java/com/ankurm/jackson3/beyond/Y04ImmutableMapperAndReaders.java)
|
|
|
|
The Jackson 2 habit of fetching the shared mapper and calling `configure()` on it has no
|
|
equivalent. Two replacements: `rebuild()` forks a builder from an existing mapper, and
|
|
`reader()`/`writer()` give per-call views.
|
|
|
|
```
|
|
shared : {"firstName":"Ada","lastName":"Lovelace","middleName":null}
|
|
rebuilt snake_case : {"first_name":"Ada","last_name":"Lovelace"}
|
|
shared unchanged : {"firstName":"Ada","lastName":"Lovelace","middleName":null}
|
|
writer view pretty : { "firstName" : "Ada", "lastName" : "Lovelace", "middleName" : null }
|
|
reader view strict : rejected (UnrecognizedPropertyException) without touching the shared mapper
|
|
```
|
|
|
|
`ObjectReader` has no `readValue(String, Class)` overload — use `.forType(X.class)`
|
|
then `.readValue(json)`.
|
|
|
|
## Y05 — creator detection
|
|
|
|
[`Y05CreatorDetection.java`](../src/main/java/com/ankurm/jackson3/beyond/Y05CreatorDetection.java)
|
|
|
|
The comparison post says removing `MapperFeature.AUTO_DETECT_CREATORS` means "any class
|
|
relying on a single-argument constructor being detected without an annotation will
|
|
quietly fail". Half right: the enum constant is gone, but the behaviour is not.
|
|
|
|
```
|
|
MapperFeature.AUTO_DETECT_CREATORS exists : false
|
|
Nearest surviving features : [INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES, DETECT_PARAMETER_NAMES, SORT_CREATOR_PROPERTIES_FIRST]
|
|
|
|
implicit single-arg ctor : ImplicitOrderId[ord-1]
|
|
explicit @JsonCreator : ExplicitOrderId[ord-2]
|
|
round-trip via @JsonValue: "ord-3"
|
|
```
|
|
|
|
Annotate with `@JsonCreator` anyway — it is explicit and costs nothing — but do not
|
|
budget upgrade time for classes that will not actually break.
|
|
|
|
## Y06 — RecyclerPool
|
|
|
|
[`Y06RecyclerPoolTuning.java`](../src/main/java/com/ankurm/jackson3/beyond/Y06RecyclerPoolTuning.java)
|
|
|
|
Jackson 3 changed the default buffer pool. The post says to restore the 2.x thread-local
|
|
pool if you see a regression, but does not measure it. Whether it helps depends entirely
|
|
on your concurrency profile, so measure on your own hardware.
|
|
|
|
```
|
|
default pool : ConcurrentDequePool
|
|
cores : 2
|
|
|
|
--- 1 thread(s), 40000 round-trips each ---
|
|
threadLocalPool (2.x default) 301 ms
|
|
concurrentDeque (3.x default) 310 ms
|
|
nonRecyclingPool (no reuse) 496 ms
|
|
|
|
--- 8 thread(s), 40000 round-trips each ---
|
|
threadLocalPool (2.x default) 451 ms
|
|
concurrentDeque (3.x default) 450 ms
|
|
nonRecyclingPool (no reuse) 808 ms
|
|
```
|
|
|
|
On this 2-core container the two pools land within noise of each other in both shapes,
|
|
and which one wins moves between runs — so the honest reading is that there is no default
|
|
winner here and the post's advice is worth testing rather than applying blind. Re-run this
|
|
a few times before concluding anything; the figures above are a single shot.
|
|
|
|
What *is* stable across runs is the third line: disabling recycling entirely is
|
|
consistently the slowest, and by a wide margin under concurrency. That is the useful
|
|
negative control — it confirms the pool is doing real work, so the choice between the two
|
|
pooling strategies is a tuning decision rather than a correctness one.
|