1
0

Jackson 3 series companion code

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.
This commit is contained in:
2026-08-04 23:12:11 +05:30
commit c438afc33b
94 changed files with 3387 additions and 0 deletions

123
README.md Normal file
View File

@@ -0,0 +1,123 @@
# jackson3-by-example
Runnable companion code for the eight-part **Jackson 3** series on [ankurm.com](https://ankurm.com).
Every example is a standalone `main()` you can run on its own. Every line of output
in [`docs/`](docs) was produced by [`run-all.sh`](run-all.sh) on the versions below —
nothing is transcribed by hand.
```
Jackson 3.2.1 (tools.jackson.core:jackson-databind)
jackson-annotations 2.22 (com.fasterxml.jackson.core — deliberately unchanged)
JDK Temurin 21.0.5
Maven 3.9.9
```
## Run it
```bash
git clone https://ankurm.com/git.app/asmhatre/jackson3-by-example.git
cd jackson3-by-example
./run-all.sh # compiles, runs all 37 examples, refreshes docs/output/
```
Or run any single example:
```bash
mvn -q compile
mvn -q exec:java -Dexec.mainClass=com.ankurm.jackson3.part5polymorphic.F01SerialiseMixedList
```
## Map: post → code
| Post | Package | Examples | Notes & output |
|---|---|---|---|
| [Jackson 101](https://ankurm.com/jackson-java-tutorial/) | [`part0setup`](src/main/java/com/ankurm/jackson3/part0setup) | [A01](src/main/java/com/ankurm/jackson3/part0setup/A01FirstRoundTrip.java) [A02](src/main/java/com/ankurm/jackson3/part0setup/A02SharedMapperConfiguration.java) [A03](src/main/java/com/ankurm/jackson3/part0setup/A03ThreeProcessingModels.java) | [write-up](docs/part0-setup.md) |
| [ObjectMapper Guide](https://ankurm.com/jackson-objectmapper-guide/) | [`part1objectmapper`](src/main/java/com/ankurm/jackson3/part1objectmapper) | [B01](src/main/java/com/ankurm/jackson3/part1objectmapper/B01WriteJson.java) [B02](src/main/java/com/ankurm/jackson3/part1objectmapper/B02ReadJson.java) [B03](src/main/java/com/ankurm/jackson3/part1objectmapper/B03GenericCollections.java) [B04](src/main/java/com/ankurm/jackson3/part1objectmapper/B04PropertyOrdering.java) | [write-up](docs/part1-objectmapper.md) |
| [Records, Optionals, Sealed](https://ankurm.com/jackson-java-records-optionals/) | [`part2modernjava`](src/main/java/com/ankurm/jackson3/part2modernjava) | [C01](src/main/java/com/ankurm/jackson3/part2modernjava/C01RecordRoundTrip.java) [C02](src/main/java/com/ankurm/jackson3/part2modernjava/C02OptionalFields.java) [C03](src/main/java/com/ankurm/jackson3/part2modernjava/C03SealedWithSubTypes.java) [C04](src/main/java/com/ankurm/jackson3/part2modernjava/C04SealedAutoDiscovery.java) | [write-up](docs/part2-modern-java.md) |
| [Annotations Cheat Sheet](https://ankurm.com/jackson-annotations-guide/) | [`part3annotations`](src/main/java/com/ankurm/jackson3/part3annotations) | [D01](src/main/java/com/ankurm/jackson3/part3annotations/D01RenameAndIgnore.java) [D02](src/main/java/com/ankurm/jackson3/part3annotations/D02InclusionAndFormat.java) [D03](src/main/java/com/ankurm/jackson3/part3annotations/D03AliasAndUnknownFields.java) [D04](src/main/java/com/ankurm/jackson3/part3annotations/D04CreatorsAndUnwrapping.java) | [write-up](docs/part3-annotations.md) |
| [Custom Serialisers & Mix-ins](https://ankurm.com/jackson-custom-serializer-mixin/) | [`part4custom`](src/main/java/com/ankurm/jackson3/part4custom) | [E01](src/main/java/com/ankurm/jackson3/part4custom/E01MoneyValueSerializer.java) [E02](src/main/java/com/ankurm/jackson3/part4custom/E02MoneyValueDeserializer.java) [E03](src/main/java/com/ankurm/jackson3/part4custom/E03SimpleModuleRegistration.java) [E04](src/main/java/com/ankurm/jackson3/part4custom/E04MixinAnnotations.java) [E05](src/main/java/com/ankurm/jackson3/part4custom/E05ValueSerializerDirect.java) | [write-up](docs/part4-custom.md) |
| [Polymorphic Deserialisation](https://ankurm.com/jackson-polymorphic-deserialization/) | [`part5polymorphic`](src/main/java/com/ankurm/jackson3/part5polymorphic) | [F01](src/main/java/com/ankurm/jackson3/part5polymorphic/F01SerialiseMixedList.java) [F02](src/main/java/com/ankurm/jackson3/part5polymorphic/F02DeserialiseMixedList.java) [F03](src/main/java/com/ankurm/jackson3/part5polymorphic/F03IncludeStrategies.java) [F04](src/main/java/com/ankurm/jackson3/part5polymorphic/F04UnknownTypeId.java) | [write-up](docs/part5-polymorphic.md) |
| [Streaming API & JsonNode](https://ankurm.com/jackson-streaming-api-jsonnode/) | [`part6streaming`](src/main/java/com/ankurm/jackson3/part6streaming) | [G01](src/main/java/com/ankurm/jackson3/part6streaming/G01StreamingParserFilter.java) [G02](src/main/java/com/ankurm/jackson3/part6streaming/G02StreamingGenerator.java) [G03](src/main/java/com/ankurm/jackson3/part6streaming/G03TreeModelNavigation.java) [G04](src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java) | [write-up](docs/part6-streaming.md) |
| [Security Best Practices](https://ankurm.com/jackson-security-best-practices/) | [`part7security`](src/main/java/com/ankurm/jackson3/part7security) | [H01](src/main/java/com/ankurm/jackson3/part7security/H01SafePolymorphismByAnnotation.java) [H02](src/main/java/com/ankurm/jackson3/part7security/H02DefaultTypingRemoved.java) [H03](src/main/java/com/ankurm/jackson3/part7security/H03PolymorphicTypeValidatorAllowlist.java) [H04](src/main/java/com/ankurm/jackson3/part7security/H04StreamReadConstraints.java) [H05](src/main/java/com/ankurm/jackson3/part7security/H05NeverDeserialiseIntoObject.java) | [write-up](docs/part7-security.md) |
| — (beyond the posts) | [`beyond`](src/main/java/com/ankurm/jackson3/beyond) | [Y01](src/main/java/com/ankurm/jackson3/beyond/Y01UncheckedExceptions.java) [Y02](src/main/java/com/ankurm/jackson3/beyond/Y02TrailingTokens.java) [Y03](src/main/java/com/ankurm/jackson3/beyond/Y03DateTimeDefaults.java) [Y04](src/main/java/com/ankurm/jackson3/beyond/Y04ImmutableMapperAndReaders.java) [Y05](src/main/java/com/ankurm/jackson3/beyond/Y05CreatorDetection.java) [Y06](src/main/java/com/ankurm/jackson3/beyond/Y06RecyclerPoolTuning.java) | [write-up](docs/beyond.md) |
### Documentation
Each page pairs the code with its real captured output and calls out where the post and
the library disagree.
| Page | Covers |
|---|---|
| [docs/part0-setup.md](docs/part0-setup.md) | Setup, the shared-mapper rule, three processing models |
| [docs/part1-objectmapper.md](docs/part1-objectmapper.md) | Reading, writing, `TypeReference`, property ordering |
| [docs/part2-modern-java.md](docs/part2-modern-java.md) | Records, `Optional`, sealed types and auto-discovery |
| [docs/part3-annotations.md](docs/part3-annotations.md) | The annotation set, and which advice is now obsolete |
| [docs/part4-custom.md](docs/part4-custom.md) | `ValueSerializer`, `ValueDeserializer`, modules, mix-ins |
| [docs/part5-polymorphic.md](docs/part5-polymorphic.md) | `@JsonTypeInfo`, and the dropped-discriminator defect in full |
| [docs/part6-streaming.md](docs/part6-streaming.md) | Streaming, the tree model, and the three approaches measured |
| [docs/part7-security.md](docs/part7-security.md) | Safe polymorphism, allowlists, resource limits |
| [docs/beyond.md](docs/beyond.md) | Unchecked exceptions, flipped defaults, pool tuning |
Raw stdout for all 37 programs is in [`docs/output/`](docs/output), regenerated by
[`run-all.sh`](run-all.sh).
## Corrections to the posts
Writing this code against a real Jackson 3.2.1 build surfaced places where the blog
snippets do not compile or do not behave as printed. Each is demonstrated by a program,
so you can verify rather than take my word for it.
| # | Claim in the post | What actually happens | Shown by |
|---|---|---|---|
| 1 | Maven coordinate `com.fasterxml.jackson.core:jackson-databind:3.1.2` | That artifact does not exist. Jackson 3 is at `tools.jackson.core:jackson-databind`; only `jackson-annotations` keeps the old group ID. | [`pom.xml`](pom.xml) |
| 2 | `JsonMapper.builder().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)` | Does not compile. The constant is not on `SerializationFeature` in Jackson 3 — it moved to `tools.jackson.databind.cfg.DateTimeFeature`, and it already defaults to off, so ISO-8601 needs no configuration. | [Y03](src/main/java/com/ankurm/jackson3/beyond/Y03DateTimeDefaults.java) |
| 3 | `JsonMapper.builder().serializationInclusion(...)` | No such builder method. The real API is `changeDefaultPropertyInclusion(UnaryOperator)`. | [A02](src/main/java/com/ankurm/jackson3/part0setup/A02SharedMapperConfiguration.java), [H02](src/main/java/com/ankurm/jackson3/part7security/H02DefaultTypingRemoved.java) |
| 4 | `writeValueAsString(List<PaymentMethod>)` emits `paymentType` | It does not. A `List` carries no element type, so the polymorphic serialiser never engages and the discriminator is dropped — and the output then fails to deserialise. Use `writerFor(TypeReference)` or a typed array. | [F01](src/main/java/com/ankurm/jackson3/part5polymorphic/F01SerialiseMixedList.java), [F02](src/main/java/com/ankurm/jackson3/part5polymorphic/F02DeserialiseMixedList.java) |
| 5 | Remediate with `mapper.activateDefaultTyping(validator, ...)` | Jackson 3's mapper has no mutators at all — not `enableDefaultTyping`, not `activateDefaultTyping`, not one `set*` method. `activateDefaultTyping` exists only on `JsonMapper.Builder`. | [H02](src/main/java/com/ankurm/jackson3/part7security/H02DefaultTypingRemoved.java), [H03](src/main/java/com/ankurm/jackson3/part7security/H03PolymorphicTypeValidatorAllowlist.java) |
| 6 | `import tools.jackson.core.JsonFactory` | Wrong package. It is `tools.jackson.core.json.JsonFactory`. | [G01](src/main/java/com/ankurm/jackson3/part6streaming/G01StreamingParserFilter.java) |
| 7 | Optional needs `jackson-datatype-jdk8`; records need `jackson-module-parameter-names` and `-parameters` | All three are built into `jackson-databind` 3.x. The Jackson 2 module classes do not exist under `tools.jackson`, so leaving the registrations in place is a compile error. | [C01](src/main/java/com/ankurm/jackson3/part2modernjava/C01RecordRoundTrip.java), [C02](src/main/java/com/ankurm/jackson3/part2modernjava/C02OptionalFields.java) |
| 8 | Set `FAIL_ON_UNKNOWN_PROPERTIES=false`; use `@JsonIgnoreProperties(ignoreUnknown=true)` to opt out per class | Already false by default in Jackson 3. The annotation now matters only when you deliberately turn strictness back on. | [D03](src/main/java/com/ankurm/jackson3/part3annotations/D03AliasAndUnknownFields.java) |
| 9 | Without `@JsonFormat`, `LocalDate` is written as a numeric array | Jackson 2 behaviour. Jackson 3 writes ISO-8601 by default; the annotation is only needed for a non-standard pattern. | [D02](src/main/java/com/ankurm/jackson3/part3annotations/D02InclusionAndFormat.java) |
| 10 | Removing `AUTO_DETECT_CREATORS` means single-arg constructors "quietly fail" | The enum constant is gone, but the behaviour is not: a single-argument constructor is still detected as a delegating creator. Annotate anyway, but the upgrade will not break these classes. | [Y05](src/main/java/com/ankurm/jackson3/beyond/Y05CreatorDetection.java) |
| 11 | The custom deserialiser uses `p.getCodec().readTree(p)` and `node.get(...)` | `getCodec()` is gone — use `ctxt.readTree(parser)`. And `get()` NPEs on a missing field; `path()` with a defaulting accessor does not. A bare `decimalValue()` on a `MissingNode` throws in Jackson 3 where Jackson 2 returned zero. | [E02](src/main/java/com/ankurm/jackson3/part4custom/E02MoneyValueDeserializer.java), [E03](src/main/java/com/ankurm/jackson3/part4custom/E03SimpleModuleRegistration.java) |
Two smaller ones worth knowing, neither strictly an error in the posts:
- A getter-based POJO serialises its properties **alphabetically**; a record serialises in
declaration order. The post's sample output for `ProductSummary` shows declaration order.
See [B04](src/main/java/com/ankurm/jackson3/part1objectmapper/B04PropertyOrdering.java).
- `{"amount":20.00}` deserialises to `BigDecimal` **20.0**, not 20.00 — the scale is lost
unless `USE_BIG_DECIMAL_FOR_FLOATS` is enabled. See
[E03](src/main/java/com/ankurm/jackson3/part4custom/E03SimpleModuleRegistration.java).
## Measured, not asserted
Three examples produce numbers rather than prose. Figures below are from one run on a
2-core container; re-run them on your own hardware.
**Three processing models over a 20 MB / 200k-entry file** ([G04](src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java)):
```
data binding (readValue) errors=4000 524 ms heap delta 47 MB
tree model (readTree) errors=4000 341 ms heap delta 102 MB
streaming (JsonParser) errors=4000 78 ms heap delta 1 MB
```
**Streaming a million records** ([G02](src/main/java/com/ankurm/jackson3/part6streaming/G02StreamingGenerator.java)): 30 MB written in 129 ms with a 0 MB heap delta.
**RecyclerPool** ([Y06](src/main/java/com/ankurm/jackson3/beyond/Y06RecyclerPoolTuning.java)) — the comparison post suggests restoring the 2.x
thread-local pool if you see a regression. On this box the two pools are within noise of
each other in both shapes, and the gap moves between runs, so the honest conclusion is
that there is no default winner: measure on your own hardware before changing it. What
is stable across runs is that turning recycling off entirely is clearly worse — a useful
negative control confirming the pool is doing something.
```
1 thread threadLocalPool 301 ms concurrentDeque 310 ms nonRecycling 496 ms
8 threads threadLocalPool 451 ms concurrentDeque 450 ms nonRecycling 808 ms
```
## Related
- [jackson2-to-3-migration](https://ankurm.com/git.app/asmhatre/jackson2-to-3-migration) — the before/after companion for the two migration guides.