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.
jackson3-by-example
Runnable companion code for the eight-part Jackson 3 series on ankurm.com.
Every example is a standalone main() you can run on its own. Every line of output
in docs/ was produced by 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
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:
mvn -q compile
mvn -q exec:java -Dexec.mainClass=com.ankurm.jackson3.part5polymorphic.F01SerialiseMixedList
Map: post → code
| Post | Package | Examples | Notes & output |
|---|---|---|---|
| Jackson 101 | part0setup |
A01 A02 A03 | write-up |
| ObjectMapper Guide | part1objectmapper |
B01 B02 B03 B04 | write-up |
| Records, Optionals, Sealed | part2modernjava |
C01 C02 C03 C04 | write-up |
| Annotations Cheat Sheet | part3annotations |
D01 D02 D03 D04 | write-up |
| Custom Serialisers & Mix-ins | part4custom |
E01 E02 E03 E04 E05 | write-up |
| Polymorphic Deserialisation | part5polymorphic |
F01 F02 F03 F04 | write-up |
| Streaming API & JsonNode | part6streaming |
G01 G02 G03 G04 | write-up |
| Security Best Practices | part7security |
H01 H02 H03 H04 H05 | write-up |
| — (beyond the posts) | beyond |
Y01 Y02 Y03 Y04 Y05 Y06 | write-up |
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 | Setup, the shared-mapper rule, three processing models |
| docs/part1-objectmapper.md | Reading, writing, TypeReference, property ordering |
| docs/part2-modern-java.md | Records, Optional, sealed types and auto-discovery |
| docs/part3-annotations.md | The annotation set, and which advice is now obsolete |
| docs/part4-custom.md | ValueSerializer, ValueDeserializer, modules, mix-ins |
| docs/part5-polymorphic.md | @JsonTypeInfo, and the dropped-discriminator defect in full |
| docs/part6-streaming.md | Streaming, the tree model, and the three approaches measured |
| docs/part7-security.md | Safe polymorphism, allowlists, resource limits |
| docs/beyond.md | Unchecked exceptions, flipped defaults, pool tuning |
Raw stdout for all 37 programs is in docs/output/, regenerated by
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 |
| 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 |
| 3 | JsonMapper.builder().serializationInclusion(...) |
No such builder method. The real API is changeDefaultPropertyInclusion(UnaryOperator). |
A02, H02 |
| 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, F02 |
| 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, H03 |
| 6 | import tools.jackson.core.JsonFactory |
Wrong package. It is tools.jackson.core.json.JsonFactory. |
G01 |
| 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, C02 |
| 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 |
| 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 |
| 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 |
| 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, E03 |
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
ProductSummaryshows declaration order. See B04. {"amount":20.00}deserialises toBigDecimal20.0, not 20.00 — the scale is lost unlessUSE_BIG_DECIMAL_FOR_FLOATSis enabled. See E03.
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):
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): 30 MB written in 129 ms with a 0 MB heap delta.
RecyclerPool (Y06) — 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 — the before/after companion for the two migration guides.