# 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.