1
0

Jackson 2 to 3 migration companion code

Three Maven modules - jackson2-before (2.22.1), jackson3-after (3.2.1) and a
coexistence module with BOTH majors on one classpath - so every claim in the two
migration guides is executed rather than asserted. Paired class names make the
before/after outputs directly diffable via run-all.sh.

Confirms the guides on wire-format equivalence (10-case suite, zero mismatches),
classpath coexistence and the collapse of four artifacts into one. Corrects nine
points, including that enableDefaultTyping() is still present in Jackson 2.22.1
rather than removed in 2.16, and that the published "after" mapper snippet does
not compile.
This commit is contained in:
2026-08-04 23:12:25 +05:30
commit f0a7053fc9
43 changed files with 1665 additions and 0 deletions

103
README.md Normal file
View File

@@ -0,0 +1,103 @@
# jackson2-to-3-migration
Companion code for the two Jackson migration guides on [ankurm.com](https://ankurm.com):
- [Jackson 2 to Jackson 3 Migration Guide](https://ankurm.com/jackson-3-migration-guide/)
- [Jackson 3 vs Jackson 2: Complete Comparison](https://ankurm.com/jackson-3-vs-jackson-2/)
The point of this repo is that nothing here is asserted. Every migration claim is a
program that runs on **both** major versions, so you can diff the outputs.
```
Jackson 2 2.22.1 (com.fasterxml.jackson.core)
Jackson 3 3.2.1 (tools.jackson.core)
jackson-annotations 2.22 (shared by both)
JDK Temurin 21.0.5
Maven 3.9.9
```
## Layout
| Module | What it is |
|---|---|
| [`jackson2-before/`](jackson2-before) | Jackson 2.22.1, with the three extra modules and the `-parameters` compiler flag it needs — see [pom.xml](jackson2-before/pom.xml) |
| [`jackson3-after/`](jackson3-after) | Jackson 3.2.1. One dependency, no modules, no compiler flags — see [pom.xml](jackson3-after/pom.xml) |
| [`coexistence/`](coexistence) | **Both majors on one classpath**, used together in one class — see [pom.xml](coexistence/pom.xml) and [docs/coexistence.md](docs/coexistence.md) |
Paired examples share a class name across `before` and `after`, so their captured
outputs sit side by side and diff cleanly.
## Run it
```bash
git clone https://ankurm.com/git.app/asmhatre/jackson2-to-3-migration.git
cd jackson2-to-3-migration
./run-all.sh
```
`run-all.sh` builds all three modules, runs every example, writes real stdout to
`docs/output/<module>-<Class>.txt`, and then reports which before/after pairs differ.
Diff any pair yourself:
```bash
diff docs/output/before-S07DefaultsThatFlipped.txt \
docs/output/after-S07DefaultsThatFlipped.txt
```
## The examples
| Pair | Migration topic | Write-up |
|---|---|---|
| `S01MapperConstruction` | `new ObjectMapper()``JsonMapper.builder()`, and immutability | [docs/s01-mapper-construction.md](docs/s01-mapper-construction.md) |
| `S02ExceptionHierarchy` | `JsonProcessingException` (checked) → `JacksonException` (unchecked) | [docs/s02-exception-hierarchy.md](docs/s02-exception-hierarchy.md) |
| `S03CustomHandlers` | `JsonSerializer`/`JsonDeserializer``ValueSerializer`/`ValueDeserializer` | [docs/s03-custom-handlers.md](docs/s03-custom-handlers.md) |
| `S04ModulesAndRecords` | Four artifacts and a compiler flag collapse to one dependency | [docs/s04-modules-and-records.md](docs/s04-modules-and-records.md) |
| `S05DefaultTyping` | `activateDefaultTyping` moves to the builder | [docs/s05-default-typing.md](docs/s05-default-typing.md) |
| `S07DefaultsThatFlipped` | Defaults that changed silently | [docs/s07-defaults-that-flipped.md](docs/s07-defaults-that-flipped.md) |
| `S06BothOnOneClasspath` | Both majors in one JVM, one shared annotations jar | [docs/coexistence.md](docs/coexistence.md) |
| `S08WireFormatEquivalence` | 10-case regression suite asserting byte-identical JSON | [docs/coexistence.md](docs/coexistence.md) |
Each write-up shows the Jackson 2 and Jackson 3 output side by side, with the source
files linked. Start at [docs/README.md](docs/README.md) for the index, or dive straight
into [docs/s07-defaults-that-flipped.md](docs/s07-defaults-that-flipped.md) — the silent
behavioural changes are the ones that cost the most to find later. Raw stdout for every
program is in [docs/output/](docs/output).
## Corrections to the guides
Building this against real artifacts surfaced several places where the guides are
wrong. Each is demonstrated by code.
| # | Claim in the guide | What actually happens |
|---|---|---|
| 1 | `.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)` in the "after" snippet | Does not compile. Not on `SerializationFeature` in Jackson 3 — moved to `tools.jackson.databind.cfg.DateTimeFeature`, and already off by default. See [`after/S01`](jackson3-after/src/main/java/com/ankurm/migration/after/S01MapperConstruction.java). |
| 2 | `JsonMapper.builder().serializationInclusion(NON_NULL)` | No such builder method. Use `changeDefaultPropertyInclusion(UnaryOperator)`. |
| 3 | `mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL, ...)` | Two errors: the method is not on the Jackson 3 mapper (builder only), and `DefaultTyping` is now a top-level enum. See [`after/S05`](jackson3-after/src/main/java/com/ankurm/migration/after/S05DefaultTyping.java). |
| 4 | `enableDefaultTyping()` was "removed in 2.16" | Still present on `ObjectMapper` in Jackson 2.22.1, deprecated. [`before/S05`](jackson2-before/src/main/java/com/ankurm/migration/before/S05DefaultTyping.java) prints the reflective proof. This matters: the method survives your 2.x upgrades and only breaks at Jackson 3. |
| 5 | `import tools.jackson.core.JsonFactory` | Wrong package — it is `tools.jackson.core.json.JsonFactory`. |
| 6 | "Java baseline: Java 11 minimum" (migration guide) | The comparison post says 17, and 17 is correct for all core modules. The migration guide's Java 11 figure is Jackson 2's. |
| 7 | `jackson-annotations` version `3.0` / `2.20.0` | Jackson 3.2.1 resolves `jackson-annotations` **2.22**. There is no released 3.0 — only release candidates. Import `tools.jackson:jackson-bom` and let it pick. |
| 8 | "`catch (IOException)` blocks silently stop catching" | Only when the block also does real I/O. If it contains nothing but Jackson calls, it is a **compile error** and the compiler catches it for you. Both cases in [`after/S02`](jackson3-after/src/main/java/com/ankurm/migration/after/S02ExceptionHierarchy.java). |
| 9 | Removing `AUTO_DETECT_CREATORS` breaks single-arg constructors | The constant is gone; the behaviour is not. Demonstrated in the [features repo](https://ankurm.com/git.app/asmhatre/jackson3-by-example). |
## What the guides get right
Worth stating, since the list above is all corrections. These claims were tested and hold:
- **The wire format is identical.** `S08WireFormatEquivalence` runs 10 cases — records,
nested lists, maps, `java.time`, present and empty `Optional`, `NON_NULL`,
`@JsonProperty`, `@JsonFormat`, empty collections — through both mappers and asserts
string equality. Zero mismatches. Downstream consumers are unaffected by the upgrade.
- **Both majors coexist on one classpath.** `S06BothOnOneClasspath` uses both in one
class, and each reads the other's output.
- **Annotations do not change.** A single `jackson-annotations-2.22.jar` serves both.
- **Records, `Optional` and `java.time` need no modules in Jackson 3.** `before/S04`
shows a bare Jackson 2 mapper failing on all three; `after/S04` shows the same code
working with `JsonMapper.builder().build()` and nothing else.
- **Mapper immutability is real, not advisory.** `before/S01` mutates a shared mapper
after construction and the change sticks; `after/S01` shows `ObjectMapper` has zero
`set*` methods.
## Related
- [jackson3-by-example](https://ankurm.com/git.app/asmhatre/jackson3-by-example) — 37 runnable examples for the eight feature posts.