# Part 5 — Polymorphic deserialisation Post: 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 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>() {})` — 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.