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

116
docs/part7-security.md Normal file
View File

@@ -0,0 +1,116 @@
# Part 7 — Security
Post: <https://ankurm.com/jackson-security-best-practices/>
## H01 — the safe pattern
[`H01SafePolymorphismByAnnotation.java`](../src/main/java/com/ankurm/jackson3/part7security/H01SafePolymorphismByAnnotation.java)
`@JsonTypeInfo(use = Id.NAME)` plus an explicit `@JsonSubTypes` registry. The permitted
set is fixed at compile time, so no payload can introduce a class of its own.
```
email : EmailNotification[a@example.com]
sms : SmsNotification[+441234567890]
attack: rejected with InvalidTypeIdException
```
This is the recommendation, and it is correct. Reach for anything below only if this
cannot express your model.
## H02 — what survived into Jackson 3
[`H02DefaultTypingRemoved.java`](../src/main/java/com/ankurm/jackson3/part7security/H02DefaultTypingRemoved.java)
The post's remediation snippet is Jackson 2:
```java
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(safeTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL, ...);
```
Neither line compiles against Jackson 3. The mapper has no mutators at all, and
`DefaultTyping` is no longer nested inside `ObjectMapper`.
```
--- tools.jackson.databind.ObjectMapper ---
enableDefaultTyping ABSENT
activateDefaultTyping ABSENT
setSerializationInclusion ABSENT
registerModule ABSENT
addMixIn ABSENT
total set*() mutators: 0
--- tools.jackson.databind.json.JsonMapper.Builder ---
activateDefaultTyping present
deactivateDefaultTyping present
polymorphicTypeValidator present
changeDefaultPropertyInclusion present
serializationInclusion ABSENT
```
A second correction, from the migration repo rather than this one: both guides state
that `enableDefaultTyping()` was "removed in 2.16". It is still present on
`ObjectMapper` in Jackson 2.22.1, deprecated. That changes the migration story — a
Jackson 2 codebase can still be compiling against it today, so Jackson 3 is where it
finally breaks, not 2.16.
## H03 — a provable allowlist
[`H03PolymorphicTypeValidatorAllowlist.java`](../src/main/java/com/ankurm/jackson3/part7security/H03PolymorphicTypeValidatorAllowlist.java)
The post's intent in working Jackson 3 form, with the negative test the post's own AI
prompt asks for.
```
allowed written : {"@class":"com.ankurm.jackson3.part7security.H03PolymorphicTypeValidatorAllowlist$Envelope","body":{"@class":"com.ankurm.jackson3.part7security.H03PolymorphicTypeValidatorAllowlist$SafePayload","note":"ok"}}
allowed read : SafePayload[ok]
rogue : rejected with InvalidTypeIdException
```
One trap the post does not mention: with `DefaultTyping.NON_FINAL`, Jackson writes a
type id for the **root** object too, so the root class must be allowlisted as well.
Allowlisting only the payload base type makes the happy path fail, not just the attack
path — which reads like a bug in your validator when it is actually correct behaviour.
## H04 — resource limits
[`H04StreamReadConstraints.java`](../src/main/java/com/ankurm/jackson3/part7security/H04StreamReadConstraints.java)
Beyond the post. Gadget attacks are not the only deserialisation risk: a few hundred
bytes of nested brackets can exhaust the stack. Jackson 3 ships defaults for this and
they are tunable.
```
--- Jackson 3 defaults ---
max nesting depth : 500
max number length : 1000
max string length : 100000000
max name length : 50000
max doc length : -1 (-1 = unlimited)
1200-deep nesting, default limits -> rejected (StreamConstraintsException)
20-deep nesting, strict limits -> rejected (StreamConstraintsException)
5-deep nesting, strict limits -> accepted
3KB string, strict limits -> rejected (StreamConstraintsException)
```
The 100 MB default string limit and unlimited document length are generous for a
public endpoint. Tighten both if you accept untrusted JSON.
## H05 — Object.class
[`H05NeverDeserialiseIntoObject.java`](../src/main/java/com/ankurm/jackson3/part7security/H05NeverDeserialiseIntoObject.java)
```
as Object : {action=ship, quantity=3, extra={nested=[1, 2]}}
runtime type: java.util.LinkedHashMap <- a plain Map, no arbitrary class was instantiated
as DTO : MyRequestDto[action=ship, quantity=3]
bad input : rejected with InvalidFormatException
```
Worth being precise here, because the post is slightly alarming about it: on a default
Jackson 3 mapper, `readValue(json, Object.class)` yields a plain `LinkedHashMap`. No
arbitrary class is instantiated. The danger returns only once default typing is active
(H03). The rule still holds — target a specific type and you get input validation as a
side effect, as the last line shows.