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

111
docs/part5-polymorphic.md Normal file
View File

@@ -0,0 +1,111 @@
# Part 5 — Polymorphic deserialisation
Post: <https://ankurm.com/jackson-polymorphic-deserialization/>
This part contains the most consequential correction in the whole repo, so it gets the
most space.
## The defect
The post's "Serialising a Mixed List" section shows:
```java
List<PaymentMethod> payments = List.of(card, bank);
String jsonOutput = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(payments);
```
and prints output containing `"paymentType" : "credit_card"`.
It does not produce that. `writeValueAsString(Object)` sees only the runtime class of
the argument — `ImmutableCollections.ListN` — which carries no element type. Without a
declared element type Jackson never engages the polymorphic type serialiser, and the
discriminator is silently omitted from every element. No exception, no warning.
The consequence is not cosmetic. That JSON cannot be read back: the deserialiser has
no type id to dispatch on and throws `InvalidTypeIdException`. A service that writes
with the post's code and reads with the post's code does not round-trip.
## F01 — the defect and both fixes
[`F01SerialiseMixedList.java`](../src/main/java/com/ankurm/jackson3/part5polymorphic/F01SerialiseMixedList.java)
```
--- 1. single element: discriminator present ---
{"paymentType":"credit_card","amountDue":99.99,"cardNetwork":"VISA","cardNumberLastFour":"4242","paymentId":1}
--- 2. BROKEN: writeValueAsString(List) drops paymentType ---
[{"amountDue":99.99,"cardNetwork":"VISA","cardNumberLastFour":"4242","paymentId":1},{"amountDue":250.0,"bankAccountIban":"GB29NWBK60161331926819","bankName":"National Bank","paymentId":2}]
--- 3. FIX A: writerFor(TypeReference) ---
[ {
"paymentType" : "credit_card",
"amountDue" : 99.99,
"cardNetwork" : "VISA",
"cardNumberLastFour" : "4242",
"paymentId" : 1
}, {
"paymentType" : "bank_transfer",
"amountDue" : 250.0,
"bankAccountIban" : "GB29NWBK60161331926819",
"bankName" : "National Bank",
"paymentId" : 2
} ]
--- 4. FIX B: a typed array carries its component type ---
[{"paymentType":"credit_card","amountDue":99.99,"cardNetwork":"VISA","cardNumberLastFour":"4242","paymentId":1},{"paymentType":"bank_transfer","amountDue":250.0,"bankAccountIban":"GB29NWBK60161331926819","bankName":"National Bank","paymentId":2}]
```
Block 1 shows a single element serialising correctly, which is why this is easy to miss
in a unit test that only checks one object. Block 2 is the collection, discriminator
absent. Blocks 3 and 4 are the two fixes:
- `mapper.writerFor(new TypeReference<List<PaymentMethod>>() {})` — declares the element
type on the writer.
- `payments.toArray(new PaymentMethod[0])` — an array carries its component type at
runtime, so no extra declaration is needed.
## F02 — round-trip proof
[`F02DeserialiseMixedList.java`](../src/main/java/com/ankurm/jackson3/part5polymorphic/F02DeserialiseMixedList.java)
```
Card ending: 4242
Bank: National Bank
lossy JSON round-trip -> InvalidTypeIdException
correct JSON round-trip -> 2 payments, CreditCardPayment first
```
Deserialisation itself works exactly as the post describes; it is the serialisation side
that is wrong. The middle two lines are the demonstration: lossy output fails, correctly
written output survives.
## F03 — the four include strategies
[`F03IncludeStrategies.java`](../src/main/java/com/ankurm/jackson3/part5polymorphic/F03IncludeStrategies.java)
The post gives these as a table. Here they are executed, with the read-back to confirm
each wire format is symmetric.
```
PROPERTY : {"kind":"card","amountDue":99.0}
WRAPPER_OBJECT : {"card":{"amountDue":99.0}}
WRAPPER_ARRAY : ["card",{"amountDue":99.0}]
EXISTING_PROPERTY : {"kind":"card","amountDue":99.0}
read PROPERTY -> PropCard[amountDue=99.0]
read WRAPPER_OBJECT -> WrapObjCard[amountDue=99.0]
read WRAPPER_ARRAY -> WrapArrCard[amountDue=99.0]
```
`EXISTING_PROPERTY` needs `visible = true` and a real field of that name on the type;
without it the discriminator is written but not populated back onto the object.
## F04 — bad discriminators
[`F04UnknownTypeId.java`](../src/main/java/com/ankurm/jackson3/part5polymorphic/F04UnknownTypeId.java)
```
unknown logical name -> InvalidTypeIdException
attacker-supplied class name -> InvalidTypeIdException
missing discriminator -> InvalidTypeIdException
```
All three fail the same way, which is the reassuring answer: an unregistered logical
name, an attacker-supplied fully qualified class name, and a missing discriminator are
indistinguishable to the type resolver, and none of them instantiate anything.