part5polymorphic: add F05-F08 (default typing modes, type validation, sealed hierarchies, unknown subtypes) for the rewritten polymorphism post (#59)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JXVi2GMQ7bR5EmbUFdDj7N
This commit is contained in:
Claude
2026-10-09 10:35:38 +00:00
parent 84070a8dfb
commit b1663c235d
9 changed files with 277 additions and 3 deletions
+3 -3
View File
@@ -18,7 +18,7 @@ Maven 3.9.9
```bash ```bash
git clone https://ankurm.com/git.app/asmhatre/jackson3-by-example.git git clone https://ankurm.com/git.app/asmhatre/jackson3-by-example.git
cd jackson3-by-example cd jackson3-by-example
./run-all.sh # compiles, runs all 39 examples, refreshes docs/output/ ./run-all.sh # compiles, runs all 43 examples, refreshes docs/output/
``` ```
Or run any single example: Or run any single example:
@@ -37,7 +37,7 @@ mvn -q exec:java -Dexec.mainClass=com.ankurm.jackson3.part5polymorphic.F01Serial
| [Records, Optionals, Sealed](https://ankurm.com/jackson-java-records-optionals/) | [`part2modernjava`](src/main/java/com/ankurm/jackson3/part2modernjava) | [C01](src/main/java/com/ankurm/jackson3/part2modernjava/C01RecordRoundTrip.java) [C02](src/main/java/com/ankurm/jackson3/part2modernjava/C02OptionalFields.java) [C03](src/main/java/com/ankurm/jackson3/part2modernjava/C03SealedWithSubTypes.java) [C04](src/main/java/com/ankurm/jackson3/part2modernjava/C04SealedAutoDiscovery.java) | [write-up](docs/part2-modern-java.md) | | [Records, Optionals, Sealed](https://ankurm.com/jackson-java-records-optionals/) | [`part2modernjava`](src/main/java/com/ankurm/jackson3/part2modernjava) | [C01](src/main/java/com/ankurm/jackson3/part2modernjava/C01RecordRoundTrip.java) [C02](src/main/java/com/ankurm/jackson3/part2modernjava/C02OptionalFields.java) [C03](src/main/java/com/ankurm/jackson3/part2modernjava/C03SealedWithSubTypes.java) [C04](src/main/java/com/ankurm/jackson3/part2modernjava/C04SealedAutoDiscovery.java) | [write-up](docs/part2-modern-java.md) |
| [Annotations Cheat Sheet](https://ankurm.com/jackson-annotations-guide/) | [`part3annotations`](src/main/java/com/ankurm/jackson3/part3annotations) | [D01](src/main/java/com/ankurm/jackson3/part3annotations/D01RenameAndIgnore.java) [D02](src/main/java/com/ankurm/jackson3/part3annotations/D02InclusionAndFormat.java) [D03](src/main/java/com/ankurm/jackson3/part3annotations/D03AliasAndUnknownFields.java) [D04](src/main/java/com/ankurm/jackson3/part3annotations/D04CreatorsAndUnwrapping.java) | [write-up](docs/part3-annotations.md) | | [Annotations Cheat Sheet](https://ankurm.com/jackson-annotations-guide/) | [`part3annotations`](src/main/java/com/ankurm/jackson3/part3annotations) | [D01](src/main/java/com/ankurm/jackson3/part3annotations/D01RenameAndIgnore.java) [D02](src/main/java/com/ankurm/jackson3/part3annotations/D02InclusionAndFormat.java) [D03](src/main/java/com/ankurm/jackson3/part3annotations/D03AliasAndUnknownFields.java) [D04](src/main/java/com/ankurm/jackson3/part3annotations/D04CreatorsAndUnwrapping.java) | [write-up](docs/part3-annotations.md) |
| [Custom Serialisers & Mix-ins](https://ankurm.com/jackson-custom-serializer-mixin/) | [`part4custom`](src/main/java/com/ankurm/jackson3/part4custom) | [E01](src/main/java/com/ankurm/jackson3/part4custom/E01MoneyValueSerializer.java) [E02](src/main/java/com/ankurm/jackson3/part4custom/E02MoneyValueDeserializer.java) [E03](src/main/java/com/ankurm/jackson3/part4custom/E03SimpleModuleRegistration.java) [E04](src/main/java/com/ankurm/jackson3/part4custom/E04MixinAnnotations.java) [E05](src/main/java/com/ankurm/jackson3/part4custom/E05ValueSerializerDirect.java) [E06](src/main/java/com/ankurm/jackson3/part4custom/E06ValueDeserializerDirect.java) [E07](src/main/java/com/ankurm/jackson3/part4custom/E07MixinOnRecord.java) | [write-up](docs/part4-custom.md) (now superseded by the rewritten post itself — see below) | | [Custom Serialisers & Mix-ins](https://ankurm.com/jackson-custom-serializer-mixin/) | [`part4custom`](src/main/java/com/ankurm/jackson3/part4custom) | [E01](src/main/java/com/ankurm/jackson3/part4custom/E01MoneyValueSerializer.java) [E02](src/main/java/com/ankurm/jackson3/part4custom/E02MoneyValueDeserializer.java) [E03](src/main/java/com/ankurm/jackson3/part4custom/E03SimpleModuleRegistration.java) [E04](src/main/java/com/ankurm/jackson3/part4custom/E04MixinAnnotations.java) [E05](src/main/java/com/ankurm/jackson3/part4custom/E05ValueSerializerDirect.java) [E06](src/main/java/com/ankurm/jackson3/part4custom/E06ValueDeserializerDirect.java) [E07](src/main/java/com/ankurm/jackson3/part4custom/E07MixinOnRecord.java) | [write-up](docs/part4-custom.md) (now superseded by the rewritten post itself — see below) |
| [Polymorphic Deserialisation](https://ankurm.com/jackson-polymorphic-deserialization/) | [`part5polymorphic`](src/main/java/com/ankurm/jackson3/part5polymorphic) | [F01](src/main/java/com/ankurm/jackson3/part5polymorphic/F01SerialiseMixedList.java) [F02](src/main/java/com/ankurm/jackson3/part5polymorphic/F02DeserialiseMixedList.java) [F03](src/main/java/com/ankurm/jackson3/part5polymorphic/F03IncludeStrategies.java) [F04](src/main/java/com/ankurm/jackson3/part5polymorphic/F04UnknownTypeId.java) | [write-up](docs/part5-polymorphic.md) | | [Polymorphic Deserialisation](https://ankurm.com/jackson-polymorphic-deserialization/) | [`part5polymorphic`](src/main/java/com/ankurm/jackson3/part5polymorphic) | [F01](src/main/java/com/ankurm/jackson3/part5polymorphic/F01SerialiseMixedList.java) [F02](src/main/java/com/ankurm/jackson3/part5polymorphic/F02DeserialiseMixedList.java) [F03](src/main/java/com/ankurm/jackson3/part5polymorphic/F03IncludeStrategies.java) [F04](src/main/java/com/ankurm/jackson3/part5polymorphic/F04UnknownTypeId.java) [F05](src/main/java/com/ankurm/jackson3/part5polymorphic/F05DefaultTypingModes.java) [F06](src/main/java/com/ankurm/jackson3/part5polymorphic/F06TypeValidation.java) [F07](src/main/java/com/ankurm/jackson3/part5polymorphic/F07SealedHierarchies.java) [F08](src/main/java/com/ankurm/jackson3/part5polymorphic/F08UnknownSubtypes.java) | [write-up](docs/part5-polymorphic.md) (F01–F04; F05–F08 are explained in the rewritten post, output in `docs/output/`) |
| [Streaming API & JsonNode](https://ankurm.com/jackson-streaming-api-jsonnode/) | [`part6streaming`](src/main/java/com/ankurm/jackson3/part6streaming) | [G01](src/main/java/com/ankurm/jackson3/part6streaming/G01StreamingParserFilter.java) [G02](src/main/java/com/ankurm/jackson3/part6streaming/G02StreamingGenerator.java) [G03](src/main/java/com/ankurm/jackson3/part6streaming/G03TreeModelNavigation.java) [G04](src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java) | [write-up](docs/part6-streaming.md) | | [Streaming API & JsonNode](https://ankurm.com/jackson-streaming-api-jsonnode/) | [`part6streaming`](src/main/java/com/ankurm/jackson3/part6streaming) | [G01](src/main/java/com/ankurm/jackson3/part6streaming/G01StreamingParserFilter.java) [G02](src/main/java/com/ankurm/jackson3/part6streaming/G02StreamingGenerator.java) [G03](src/main/java/com/ankurm/jackson3/part6streaming/G03TreeModelNavigation.java) [G04](src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java) | [write-up](docs/part6-streaming.md) |
| [Security Best Practices](https://ankurm.com/jackson-security-best-practices/) | [`part7security`](src/main/java/com/ankurm/jackson3/part7security) | [H01](src/main/java/com/ankurm/jackson3/part7security/H01SafePolymorphismByAnnotation.java) [H02](src/main/java/com/ankurm/jackson3/part7security/H02DefaultTypingRemoved.java) [H03](src/main/java/com/ankurm/jackson3/part7security/H03PolymorphicTypeValidatorAllowlist.java) [H04](src/main/java/com/ankurm/jackson3/part7security/H04StreamReadConstraints.java) [H05](src/main/java/com/ankurm/jackson3/part7security/H05NeverDeserialiseIntoObject.java) | [write-up](docs/part7-security.md) | | [Security Best Practices](https://ankurm.com/jackson-security-best-practices/) | [`part7security`](src/main/java/com/ankurm/jackson3/part7security) | [H01](src/main/java/com/ankurm/jackson3/part7security/H01SafePolymorphismByAnnotation.java) [H02](src/main/java/com/ankurm/jackson3/part7security/H02DefaultTypingRemoved.java) [H03](src/main/java/com/ankurm/jackson3/part7security/H03PolymorphicTypeValidatorAllowlist.java) [H04](src/main/java/com/ankurm/jackson3/part7security/H04StreamReadConstraints.java) [H05](src/main/java/com/ankurm/jackson3/part7security/H05NeverDeserialiseIntoObject.java) | [write-up](docs/part7-security.md) |
| — (beyond the posts) | [`beyond`](src/main/java/com/ankurm/jackson3/beyond) | [Y01](src/main/java/com/ankurm/jackson3/beyond/Y01UncheckedExceptions.java) [Y02](src/main/java/com/ankurm/jackson3/beyond/Y02TrailingTokens.java) [Y03](src/main/java/com/ankurm/jackson3/beyond/Y03DateTimeDefaults.java) [Y04](src/main/java/com/ankurm/jackson3/beyond/Y04ImmutableMapperAndReaders.java) [Y05](src/main/java/com/ankurm/jackson3/beyond/Y05CreatorDetection.java) [Y06](src/main/java/com/ankurm/jackson3/beyond/Y06RecyclerPoolTuning.java) | [write-up](docs/beyond.md) | | — (beyond the posts) | [`beyond`](src/main/java/com/ankurm/jackson3/beyond) | [Y01](src/main/java/com/ankurm/jackson3/beyond/Y01UncheckedExceptions.java) [Y02](src/main/java/com/ankurm/jackson3/beyond/Y02TrailingTokens.java) [Y03](src/main/java/com/ankurm/jackson3/beyond/Y03DateTimeDefaults.java) [Y04](src/main/java/com/ankurm/jackson3/beyond/Y04ImmutableMapperAndReaders.java) [Y05](src/main/java/com/ankurm/jackson3/beyond/Y05CreatorDetection.java) [Y06](src/main/java/com/ankurm/jackson3/beyond/Y06RecyclerPoolTuning.java) | [write-up](docs/beyond.md) |
@@ -59,7 +59,7 @@ the library disagree.
| [docs/part7-security.md](docs/part7-security.md) | Safe polymorphism, allowlists, resource limits | | [docs/part7-security.md](docs/part7-security.md) | Safe polymorphism, allowlists, resource limits |
| [docs/beyond.md](docs/beyond.md) | Unchecked exceptions, flipped defaults, pool tuning | | [docs/beyond.md](docs/beyond.md) | Unchecked exceptions, flipped defaults, pool tuning |
Raw stdout for all 39 programs is in [`docs/output/`](docs/output), regenerated by Raw stdout for all 43 programs is in [`docs/output/`](docs/output), regenerated by
[`run-all.sh`](run-all.sh). [`run-all.sh`](run-all.sh).
## Corrections to the posts ## Corrections to the posts
+19
View File
@@ -0,0 +1,19 @@
NONE (no default typing): {"any":{"label":"t"},"colour":"RED","dot":{"x":2},"items":["a"],"numbers":[1,2],"shape":{"x":1},"tag":{"label":"t"},"text":"s"}
JAVA_LANG_OBJECT:
{"any":["~Tag",{"label":"t"}],"colour":"RED","dot":{"x":2},"items":["a"],"numbers":[1,2],"shape":{"x":1},"tag":{"label":"t"},"text":"s"}
OBJECT_AND_NON_CONCRETE:
{"any":["~Tag",{"label":"t"}],"colour":"RED","dot":{"x":2},"items":["java.util.ImmutableCollections$List12",["a"]],"numbers":[1,2],"shape":["~Dot",{"x":1}],"tag":{"label":"t"},"text":"s"}
NON_CONCRETE_AND_ARRAYS:
{"any":["~Tag",{"label":"t"}],"colour":"RED","dot":{"x":2},"items":["java.util.ImmutableCollections$List12",["a"]],"numbers":[1,2],"shape":["~Dot",{"x":1}],"tag":{"label":"t"},"text":"s"}
NON_FINAL:
["~Holder",{"any":["~Tag",{"label":"t"}],"colour":"RED","dot":{"x":2},"items":["java.util.ImmutableCollections$List12",["a"]],"numbers":[1,2],"shape":["~Dot",{"x":1}],"tag":{"label":"t"},"text":"s"}]
NON_FINAL_AND_ENUMS:
["~Holder",{"any":["~Tag",{"label":"t"}],"colour":["~Colour","RED"],"dot":{"x":2},"items":["java.util.ImmutableCollections$List12",["a"]],"numbers":[1,2],"shape":["~Dot",{"x":1}],"tag":{"label":"t"},"text":"s"}]
NON_FINAL_AND_RECORDS:
["~Holder",{"any":["~Tag",{"label":"t"}],"colour":"RED","dot":["~Dot",{"x":2}],"items":["java.util.ImmutableCollections$List12",["a"]],"numbers":[1,2],"shape":["~Dot",{"x":1}],"tag":{"label":"t"},"text":"s"}]
+6
View File
@@ -0,0 +1,6 @@
default mapper, Good (listed in @JsonSubTypes) -> accepted Good
default mapper, Rogue (not listed) -> accepted Rogue
FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED, Good -> accepted Good
FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED, Rogue -> InvalidTypeIdException
PolymorphicTypeValidator allowing only Good, Good -> accepted Good
PolymorphicTypeValidator allowing only Good, Rogue -> InvalidTypeIdException
+11
View File
@@ -0,0 +1,11 @@
each as Event.class:
{"kind":"payment","id":"p1","amount":10.0}
{"kind":"refund","id":"r1","amount":4.0}
{"kind":"login","user":"ann"}
{"kind":"F07SealedHierarchies$Logout","user":"ann"}
round trip equal: true
payment 10.0
refund 4.0
login ann
logout ann
nested sealed type read as Audit.class: Login[user=bo]
+7
View File
@@ -0,0 +1,7 @@
defaultImpl, known name -> Click[x=3]
defaultImpl, unknown name 'swipe' -> Unknown[]
defaultImpl, class-name attack -> Unknown[]
defaultImpl, discriminator missing -> Unknown[]
FAIL_ON_INVALID_SUBTYPE off, unknown name -> null
FAIL_ON_INVALID_SUBTYPE off, missing discriminator -> null
default, unknown name -> InvalidTypeIdException
@@ -0,0 +1,59 @@
package com.ankurm.jackson3.part5polymorphic;
import tools.jackson.databind.DefaultTyping;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import tools.jackson.databind.jsontype.PolymorphicTypeValidator;
import java.util.List;
/**
* Post: Polymorphic Deserialisation — https://ankurm.com/jackson-polymorphic-deserialization/
* Section: "Default typing: what Jackson 3 changed"
*
* Default typing writes a type id for every value whose declared type matches a rule,
* with no annotation on the class. It is the mechanism the security post warns about.
* This prints what each Jackson 3 DefaultTyping constant puts on the wire for the same
* object, so the choice is made by looking rather than by name.
*
* Jackson 3 has six constants (javap on 3.2.3): JAVA_LANG_OBJECT, OBJECT_AND_NON_CONCRETE,
* NON_CONCRETE_AND_ARRAYS, NON_FINAL, NON_FINAL_AND_ENUMS, NON_FINAL_AND_RECORDS.
* The enum is top-level (tools.jackson.databind.DefaultTyping), not nested in ObjectMapper,
* and it is passed to the mapper BUILDER: a built mapper has no activateDefaultTyping.
*/
public class F05DefaultTypingModes {
public interface Shape { }
public record Dot(int x) implements Shape { }
public enum Colour { RED }
public static final class Tag { public String label = "t"; }
/** One field per kind of declared type the constants distinguish. */
public static class Holder {
public Object any = new Tag();
public Shape shape = new Dot(1);
public Dot dot = new Dot(2);
public Tag tag = new Tag();
public Colour colour = Colour.RED;
public String text = "s";
public int[] numbers = {1, 2};
public List<Object> items = List.of("a");
}
public static void main(String[] args) {
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.ankurm.")
.allowIfSubType("java.")
.allowIfSubTypeIsArray()
.build();
System.out.println("NONE (no default typing): " + JsonMapper.builder().build().writeValueAsString(new Holder()));
for (DefaultTyping mode : DefaultTyping.values()) {
JsonMapper mapper = JsonMapper.builder().activateDefaultTyping(ptv, mode).build();
System.out.println();
System.out.println(mode + ":");
System.out.println(" " + mapper.writeValueAsString(new Holder())
.replace("com.ankurm.jackson3.part5polymorphic.F05DefaultTypingModes$", "~"));
}
}
}
@@ -0,0 +1,58 @@
package com.ankurm.jackson3.part5polymorphic;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
/**
* Post: Polymorphic Deserialisation — https://ankurm.com/jackson-polymorphic-deserialization/
* Section: "Letting the JSON name the class (and why you should not)"
*
* @JsonTypeInfo(use = CLASS) lets the JSON choose any subclass by its fully qualified
* name. Two Jackson 3 controls decide which names are believed:
* - a PolymorphicTypeValidator on the mapper builder (an allow-list of types), and
* - DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED (new in Jackson 3),
* which requires the class to be listed in @JsonSubTypes.
* Every case below reads a payload naming Rogue, a legitimate Plugin subtype that the
* application never meant to accept from the wire. Nothing dangerous is instantiated;
* Rogue only has a field, which is exactly the point: the check is on the NAME.
*/
public class F06TypeValidation {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class")
@com.fasterxml.jackson.annotation.JsonSubTypes(@com.fasterxml.jackson.annotation.JsonSubTypes.Type(Good.class))
public abstract static class Plugin { }
public static class Good extends Plugin { public String note = "ok"; }
public static class Rogue extends Plugin { public String note = "rogue"; }
private static final String GOOD = "{\"@class\":\"" + Good.class.getName() + "\",\"note\":\"x\"}";
private static final String ROGUE = "{\"@class\":\"" + Rogue.class.getName() + "\",\"note\":\"x\"}";
static void read(String label, JsonMapper mapper, String json) {
try {
Plugin p = mapper.readValue(json, Plugin.class);
System.out.printf("%-58s -> accepted %s%n", label, p.getClass().getSimpleName());
} catch (Exception e) {
System.out.printf("%-58s -> %s%n", label, e.getClass().getSimpleName());
}
}
public static void main(String[] args) {
JsonMapper plain = JsonMapper.builder().build();
read("default mapper, Good (listed in @JsonSubTypes)", plain, GOOD);
read("default mapper, Rogue (not listed)", plain, ROGUE);
JsonMapper strict = JsonMapper.builder()
.enable(DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED).build();
read("FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED, Good", strict, GOOD);
read("FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED, Rogue", strict, ROGUE);
JsonMapper allowList = JsonMapper.builder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Good.class).build())
.build();
read("PolymorphicTypeValidator allowing only Good, Good", allowList, GOOD);
read("PolymorphicTypeValidator allowing only Good, Rogue", allowList, ROGUE);
}
}
@@ -0,0 +1,57 @@
package com.ankurm.jackson3.part5polymorphic;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.json.JsonMapper;
import java.util.List;
/**
* Post: Polymorphic Deserialisation — https://ankurm.com/jackson-polymorphic-deserialization/
* Section: "Sealed types: let `permits` be the registry"
*
* Builds on part2modernjava/C03 and C04. New here: a NESTED sealed hierarchy, a
* permitted type with no @JsonTypeName, and an exhaustive switch over the result.
* No @JsonSubTypes anywhere.
*/
public class F07SealedHierarchies {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind")
public sealed interface Event permits Payment, Refund, Audit { }
@JsonTypeName("payment") public record Payment(String id, double amount) implements Event { }
@JsonTypeName("refund") public record Refund(String id, double amount) implements Event { }
/** A sealed sub-hierarchy: its permitted records are discovered through Event too. */
public sealed interface Audit extends Event permits Login, Logout { }
@JsonTypeName("login") public record Login(String user) implements Audit { }
/** Deliberately NO @JsonTypeName: the id falls back to the simple class name. */
public record Logout(String user) implements Audit { }
static String describe(Event e) {
return switch (e) {
case Payment p -> "payment " + p.amount();
case Refund r -> "refund " + r.amount();
case Login l -> "login " + l.user();
case Logout l -> "logout " + l.user();
};
}
public static void main(String[] args) {
JsonMapper mapper = JsonMapper.builder().build();
List<Event> events = List.of(new Payment("p1", 10.0), new Refund("r1", 4.0),
new Login("ann"), new Logout("ann"));
System.out.println("each as Event.class:");
for (Event e : events) {
System.out.println(" " + mapper.writeValueAsString(e));
}
String json = mapper.writerFor(new TypeReference<List<Event>>() { }).writeValueAsString(events);
List<Event> back = mapper.readValue(json, new TypeReference<List<Event>>() { });
System.out.println("round trip equal: " + events.equals(back));
back.forEach(e -> System.out.println(" " + describe(e)));
System.out.println("nested sealed type read as Audit.class: "
+ mapper.readValue("{\"kind\":\"login\",\"user\":\"bo\"}", Audit.class));
}
}
@@ -0,0 +1,57 @@
package com.ankurm.jackson3.part5polymorphic;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.json.JsonMapper;
/**
* Post: Polymorphic Deserialisation — https://ankurm.com/jackson-polymorphic-deserialization/
* Section: "When a new producer sends a type you have not heard of"
*
* Extends F04 (which shows the default failure). Two ways to be tolerant of a newer
* producer, and what each costs. A tolerant reader must still never turn an unknown
* name into a class lookup; both options below keep the registry closed.
*/
public class F08UnknownSubtypes {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = Unknown.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = Click.class, name = "click"),
@JsonSubTypes.Type(value = Unknown.class, name = "unknown")})
public interface UiEvent { }
public record Click(int x) implements UiEvent { }
/** The catch-all: carries nothing, so nothing from the payload is trusted. */
public record Unknown() implements UiEvent { }
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(@JsonSubTypes.Type(value = Scroll.class, name = "scroll"))
public interface OtherEvent { }
public record Scroll(int dy) implements OtherEvent { }
static void show(String label, Object result) {
System.out.printf("%-52s -> %s%n", label, result);
}
public static void main(String[] args) {
JsonMapper mapper = JsonMapper.builder().build();
show("defaultImpl, known name", mapper.readValue("{\"type\":\"click\",\"x\":3}", UiEvent.class));
show("defaultImpl, unknown name 'swipe'", mapper.readValue("{\"type\":\"swipe\",\"dx\":9}", UiEvent.class));
show("defaultImpl, class-name attack", mapper.readValue("{\"type\":\"com.malicious.Gadget\"}", UiEvent.class));
show("defaultImpl, discriminator missing", mapper.readValue("{\"x\":3}", UiEvent.class));
JsonMapper lenient = JsonMapper.builder().disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE).build();
show("FAIL_ON_INVALID_SUBTYPE off, unknown name", lenient.readValue("{\"type\":\"pinch\"}", OtherEvent.class));
try {
show("FAIL_ON_INVALID_SUBTYPE off, missing discriminator",
lenient.readValue("{\"dy\":1}", OtherEvent.class));
} catch (Exception e) {
show("FAIL_ON_INVALID_SUBTYPE off, missing discriminator", e.getClass().getSimpleName());
}
try {
mapper.readValue("{\"type\":\"pinch\"}", OtherEvent.class);
} catch (Exception e) {
show("default, unknown name", e.getClass().getSimpleName());
}
}
}