1
0

Part 7: what readValue(json, Object.class) actually does on a default mapper

This commit is contained in:
2026-08-04 17:34:44 +00:00
parent 028b535ef6
commit 5b7f0fe126

View File

@@ -0,0 +1,42 @@
package com.ankurm.jackson3.part7security;
import tools.jackson.databind.json.JsonMapper;
/**
* Post: Jackson Security Best Practices — https://ankurm.com/jackson-security-best-practices/
* Section: "Never Deserialise Untrusted JSON into Object.class"
*
* Worth knowing precisely what readValue(json, Object.class) does on a DEFAULT
* Jackson 3 mapper, because the answer is reassuring and often misunderstood:
* with no default typing active it produces plain Maps, Lists, Strings and numbers.
* The danger only returns when default typing is switched on — as H03 shows.
*
* The rule still stands. Target a specific type; you get validation for free.
*/
public class H05NeverDeserialiseIntoObject {
public record MyRequestDto(String action, int quantity) { }
public static void main(String[] args) {
JsonMapper mapper = JsonMapper.builder().build();
String untrusted = "{\"action\":\"ship\",\"quantity\":3,\"extra\":{\"nested\":[1,2]}}";
Object loose = mapper.readValue(untrusted, Object.class);
System.out.println("as Object : " + loose);
System.out.println("runtime type: " + loose.getClass().getName()
+ " <- a plain Map, no arbitrary class was instantiated");
MyRequestDto typed = mapper.readValue(untrusted, MyRequestDto.class);
System.out.println("as DTO : " + typed);
// The real benefit of a specific target type: malformed input fails loudly
// instead of flowing onward as an untyped Map.
try {
mapper.readValue("{\"action\":\"ship\",\"quantity\":\"not-a-number\"}", MyRequestDto.class);
System.out.println("bad input : UNEXPECTEDLY ACCEPTED");
} catch (Exception e) {
System.out.println("bad input : rejected with " + e.getClass().getSimpleName());
}
}
}