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:
111
docs/part5-polymorphic.md
Normal file
111
docs/part5-polymorphic.md
Normal 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.
|
||||
Reference in New Issue
Block a user