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:
@@ -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
|
||||||
|
|||||||
@@ -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"}]
|
||||||
@@ -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
|
||||||
@@ -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]
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user