From b1663c235de615dfabecdc97f93e4517ee0ac76d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 10:35:38 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01JXVi2GMQ7bR5EmbUFdDj7N --- README.md | 6 +- docs/output/F05DefaultTypingModes.txt | 19 ++++++ docs/output/F06TypeValidation.txt | 6 ++ docs/output/F07SealedHierarchies.txt | 11 ++++ docs/output/F08UnknownSubtypes.txt | 7 +++ .../F05DefaultTypingModes.java | 59 +++++++++++++++++++ .../part5polymorphic/F06TypeValidation.java | 58 ++++++++++++++++++ .../F07SealedHierarchies.java | 57 ++++++++++++++++++ .../part5polymorphic/F08UnknownSubtypes.java | 57 ++++++++++++++++++ 9 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 docs/output/F05DefaultTypingModes.txt create mode 100644 docs/output/F06TypeValidation.txt create mode 100644 docs/output/F07SealedHierarchies.txt create mode 100644 docs/output/F08UnknownSubtypes.txt create mode 100644 src/main/java/com/ankurm/jackson3/part5polymorphic/F05DefaultTypingModes.java create mode 100644 src/main/java/com/ankurm/jackson3/part5polymorphic/F06TypeValidation.java create mode 100644 src/main/java/com/ankurm/jackson3/part5polymorphic/F07SealedHierarchies.java create mode 100644 src/main/java/com/ankurm/jackson3/part5polymorphic/F08UnknownSubtypes.java diff --git a/README.md b/README.md index abe57c0..a9c93ae 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Maven 3.9.9 ```bash git clone https://ankurm.com/git.app/asmhatre/jackson3-by-example.git 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: @@ -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) | | [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) | -| [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) | | [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) | @@ -59,7 +59,7 @@ the library disagree. | [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 | -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). ## Corrections to the posts diff --git a/docs/output/F05DefaultTypingModes.txt b/docs/output/F05DefaultTypingModes.txt new file mode 100644 index 0000000..3943418 --- /dev/null +++ b/docs/output/F05DefaultTypingModes.txt @@ -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"}] diff --git a/docs/output/F06TypeValidation.txt b/docs/output/F06TypeValidation.txt new file mode 100644 index 0000000..176a49a --- /dev/null +++ b/docs/output/F06TypeValidation.txt @@ -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 diff --git a/docs/output/F07SealedHierarchies.txt b/docs/output/F07SealedHierarchies.txt new file mode 100644 index 0000000..27a11b6 --- /dev/null +++ b/docs/output/F07SealedHierarchies.txt @@ -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] diff --git a/docs/output/F08UnknownSubtypes.txt b/docs/output/F08UnknownSubtypes.txt new file mode 100644 index 0000000..4e298aa --- /dev/null +++ b/docs/output/F08UnknownSubtypes.txt @@ -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 diff --git a/src/main/java/com/ankurm/jackson3/part5polymorphic/F05DefaultTypingModes.java b/src/main/java/com/ankurm/jackson3/part5polymorphic/F05DefaultTypingModes.java new file mode 100644 index 0000000..6287f93 --- /dev/null +++ b/src/main/java/com/ankurm/jackson3/part5polymorphic/F05DefaultTypingModes.java @@ -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 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$", "~")); + } + } +} diff --git a/src/main/java/com/ankurm/jackson3/part5polymorphic/F06TypeValidation.java b/src/main/java/com/ankurm/jackson3/part5polymorphic/F06TypeValidation.java new file mode 100644 index 0000000..c31de18 --- /dev/null +++ b/src/main/java/com/ankurm/jackson3/part5polymorphic/F06TypeValidation.java @@ -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); + } +} diff --git a/src/main/java/com/ankurm/jackson3/part5polymorphic/F07SealedHierarchies.java b/src/main/java/com/ankurm/jackson3/part5polymorphic/F07SealedHierarchies.java new file mode 100644 index 0000000..6d1f26e --- /dev/null +++ b/src/main/java/com/ankurm/jackson3/part5polymorphic/F07SealedHierarchies.java @@ -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 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>() { }).writeValueAsString(events); + List back = mapper.readValue(json, new TypeReference>() { }); + 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)); + } +} diff --git a/src/main/java/com/ankurm/jackson3/part5polymorphic/F08UnknownSubtypes.java b/src/main/java/com/ankurm/jackson3/part5polymorphic/F08UnknownSubtypes.java new file mode 100644 index 0000000..09f1399 --- /dev/null +++ b/src/main/java/com/ankurm/jackson3/part5polymorphic/F08UnknownSubtypes.java @@ -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()); + } + } +}