diff --git a/sealed/README.md b/sealed/README.md index c582ec7..1826449 100644 --- a/sealed/README.md +++ b/sealed/README.md @@ -10,6 +10,7 @@ JDK25=/path/to/jdk-25 JDK27=/path/to/jdk-27 [JDK21=/path/to/jdk-21] ./run.sh - `src/` compiles and runs unchanged on 21, 25 and 27: the `Payment` model, the three subclass modifiers, generic and recursive hierarchies, the `default` trap and a visitor for comparison. - `broken/` fails to compile on purpose (one rule per file). - `sep/` shows what the JVM itself enforces when a class file disagrees with the sealed type it claims to extend. +- `jackson/` runs the same `Payment` through Jackson 3.2.3 (`./jackson/fetch.sh` downloads and sha1-checks the three jars from Maven Central; they are not committed). - `multi/` is a named module, the only place a sealed hierarchy may span packages. Tested on Temurin 25.0.4.1+1 and 27+35, and OpenJDK 21.0.10 as the baseline. diff --git a/sealed/jackson/.gitignore b/sealed/jackson/.gitignore new file mode 100644 index 0000000..c3af857 --- /dev/null +++ b/sealed/jackson/.gitignore @@ -0,0 +1 @@ +lib/ diff --git a/sealed/jackson/JsonForgotten.java b/sealed/jackson/JsonForgotten.java new file mode 100644 index 0000000..c5d4139 --- /dev/null +++ b/sealed/jackson/JsonForgotten.java @@ -0,0 +1,25 @@ +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.json.JsonMapper; + +public class JsonForgotten { + // Wallet is a permitted subtype, and the compiler is happy, but nobody told Jackson about it. + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = Card.class, name = "card"), + @JsonSubTypes.Type(value = Upi.class, name = "upi")}) + sealed interface Payment permits Card, Upi, Wallet {} + record Card(long amount) implements Payment {} + record Upi(long amount) implements Payment {} + record Wallet(long amount) implements Payment {} + + public static void main(String[] args) { + JsonMapper mapper = JsonMapper.builder().build(); + System.out.println("write : " + mapper.writeValueAsString(new Wallet(500))); + try { + mapper.readValue("{\"type\":\"wallet\",\"amount\":500}", Payment.class); + } catch (Exception e) { + System.out.println("read : " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse("")); + } + } +} diff --git a/sealed/jackson/JsonNoSubTypes.java b/sealed/jackson/JsonNoSubTypes.java new file mode 100644 index 0000000..519a9f9 --- /dev/null +++ b/sealed/jackson/JsonNoSubTypes.java @@ -0,0 +1,32 @@ +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.json.JsonMapper; + +public class JsonNoSubTypes { + // Only @JsonTypeInfo, no @JsonSubTypes. Does Jackson find the permitted subclasses by itself? + @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME, property = "type") + sealed interface Payment permits Card, Upi {} + record Card(long amount) implements Payment {} + record Upi(long amount) implements Payment {} + + // Control: the same shape without the word sealed. + @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME, property = "type") + interface OpenPayment {} + record OpenCard(long amount) implements OpenPayment {} + + static void tryRead(JsonMapper m, String label, String json, Class type) { + try { + System.out.println(label + ": " + m.readValue(json, type)); + } catch (Exception e) { + System.out.println(label + ": " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse("")); + } + } + + public static void main(String[] args) { + JsonMapper m = JsonMapper.builder().build(); + System.out.println("write : " + m.writeValueAsString(new Card(500))); + tryRead(m, "sealed, Card ", "{\"type\":\"Card\",\"amount\":500}", Payment.class); + tryRead(m, "sealed, Upi ", "{\"type\":\"Upi\",\"amount\":500}", Payment.class); + tryRead(m, "sealed, Cheque", "{\"type\":\"Cheque\",\"amount\":500}", Payment.class); + tryRead(m, "plain, OpenCard", "{\"type\":\"OpenCard\",\"amount\":500}", OpenPayment.class); + } +} diff --git a/sealed/jackson/JsonPayment.java b/sealed/jackson/JsonPayment.java new file mode 100644 index 0000000..d7ac851 --- /dev/null +++ b/sealed/jackson/JsonPayment.java @@ -0,0 +1,39 @@ +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.json.JsonMapper; + +public class JsonPayment { + // Two annotations tell Jackson which JSON field names the type, and which class each name means. + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = Card.class, name = "card"), + @JsonSubTypes.Type(value = Upi.class, name = "upi"), + @JsonSubTypes.Type(value = NetBanking.class, name = "netbanking")}) + sealed interface Payment permits Card, Upi, NetBanking {} + record Card(String last4, long amount) implements Payment {} + record Upi(String vpa, long amount) implements Payment {} + record NetBanking(String bank, long amount) implements Payment {} + + static long fee(Payment p) { + return switch (p) { + case Card(String last4, long amount) -> amount * 2 / 100; + case Upi(String vpa, long amount) -> 0; + case NetBanking(String bank, long amount) -> 15; + }; + } + + public static void main(String[] args) { + JsonMapper mapper = JsonMapper.builder().build(); + Payment[] all = { new Card("4242", 500), new Upi("ankur@bank", 500), new NetBanking("HDFC", 500) }; + for (Payment p : all) { + String json = mapper.writeValueAsString(p); + Payment back = mapper.readValue(json, Payment.class); + System.out.println(json + " -> " + back + " equal=" + back.equals(p) + " fee=" + fee(back)); + } + try { + mapper.readValue("{\"type\":\"cheque\",\"amount\":500}", Payment.class); + } catch (Exception e) { + System.out.println("unknown type: " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse("")); + } + } +} diff --git a/sealed/jackson/fetch.sh b/sealed/jackson/fetch.sh new file mode 100755 index 0000000..0839e6b --- /dev/null +++ b/sealed/jackson/fetch.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Downloads the three Jackson jars from Maven Central into ./lib and checks each against the published .sha1 +set -euo pipefail +cd "$(dirname "$0")"; mkdir -p lib +M=https://repo1.maven.org/maven2 +for a in tools/jackson/core/jackson-databind/3.2.3/jackson-databind-3.2.3 \ + tools/jackson/core/jackson-core/3.2.3/jackson-core-3.2.3 \ + com/fasterxml/jackson/core/jackson-annotations/2.22/jackson-annotations-2.22; do + f="lib/$(basename "$a").jar" + [ -f "$f" ] || curl -sfL -o "$f" "$M/$a.jar" + want="$(curl -sfL "$M/$a.jar.sha1" | cut -d' ' -f1)"; got="$(sha1sum "$f" | cut -d' ' -f1)" + [ "$want" = "$got" ] && echo "ok $f $got" || { echo "SHA1 MISMATCH $f"; exit 1; } +done diff --git a/sealed/output/09-release-levels.txt b/sealed/output/09-release-levels.txt new file mode 100644 index 0000000..68c6a86 --- /dev/null +++ b/sealed/output/09-release-levels.txt @@ -0,0 +1,17 @@ +$ javac --release 16 src/ImplicitPermits.java (25.0.4.1+1-LTS) +src/ImplicitPermits.java:4: error: sealed classes are not supported in -source 16 +public sealed class ImplicitPermits { + ^ + (use -source 17 or higher to enable sealed classes) +1 error +exit=1 + +$ javac --release 17 src/ImplicitPermits.java (25.0.4.1+1-LTS) +exit=0 + +$ javac --release 17 src/PaymentDemo.java (25.0.4.1+1-LTS) +src/PaymentDemo.java:11: error: patterns in switch statements are not supported in -source 17 + case Card(String last4, long amount) -> amount * 2 / 100; // 2 percent + ^ + (use -source 21 or higher to enable patterns in switch statements) +src/PaymentDemo.java:11: error: deconstruction patterns are not supported in -source 17 diff --git a/sealed/output/10-jackson.txt b/sealed/output/10-jackson.txt new file mode 100644 index 0000000..0e1c0fa --- /dev/null +++ b/sealed/output/10-jackson.txt @@ -0,0 +1,50 @@ +$ javac -cp jackson/lib/* jackson/JsonPayment.java && java JsonPayment (21.0.10+7-Ubuntu-124.04) +{"type":"card","last4":"4242","amount":500} -> Card[last4=4242, amount=500] equal=true fee=10 +{"type":"upi","vpa":"ankur@bank","amount":500} -> Upi[vpa=ankur@bank, amount=500] equal=true fee=0 +{"type":"netbanking","bank":"HDFC","amount":500} -> NetBanking[bank=HDFC, amount=500] equal=true fee=15 +unknown type: InvalidTypeIdException: Could not resolve type id 'cheque' as a subtype of `JsonPayment$Payment`: known type ids = [card, netbanking, upi] + +$ javac -cp jackson/lib/* jackson/JsonPayment.java && java JsonPayment (25.0.4.1+1-LTS) +{"type":"card","last4":"4242","amount":500} -> Card[last4=4242, amount=500] equal=true fee=10 +{"type":"upi","vpa":"ankur@bank","amount":500} -> Upi[vpa=ankur@bank, amount=500] equal=true fee=0 +{"type":"netbanking","bank":"HDFC","amount":500} -> NetBanking[bank=HDFC, amount=500] equal=true fee=15 +unknown type: InvalidTypeIdException: Could not resolve type id 'cheque' as a subtype of `JsonPayment$Payment`: known type ids = [card, netbanking, upi] + +$ javac -cp jackson/lib/* jackson/JsonPayment.java && java JsonPayment (27+35) +{"type":"card","last4":"4242","amount":500} -> Card[last4=4242, amount=500] equal=true fee=10 +{"type":"upi","vpa":"ankur@bank","amount":500} -> Upi[vpa=ankur@bank, amount=500] equal=true fee=0 +{"type":"netbanking","bank":"HDFC","amount":500} -> NetBanking[bank=HDFC, amount=500] equal=true fee=15 +unknown type: InvalidTypeIdException: Could not resolve type id 'cheque' as a subtype of `JsonPayment$Payment`: known type ids = [card, netbanking, upi] + +$ javac -cp jackson/lib/* jackson/JsonForgotten.java && java JsonForgotten (21.0.10+7-Ubuntu-124.04) +write : {"type":"JsonForgotten$Wallet","amount":500} +read : InvalidTypeIdException: Could not resolve type id 'wallet' as a subtype of `JsonForgotten$Payment`: known type ids = [card, upi] + +$ javac -cp jackson/lib/* jackson/JsonForgotten.java && java JsonForgotten (25.0.4.1+1-LTS) +write : {"type":"JsonForgotten$Wallet","amount":500} +read : InvalidTypeIdException: Could not resolve type id 'wallet' as a subtype of `JsonForgotten$Payment`: known type ids = [card, upi] + +$ javac -cp jackson/lib/* jackson/JsonForgotten.java && java JsonForgotten (27+35) +write : {"type":"JsonForgotten$Wallet","amount":500} +read : InvalidTypeIdException: Could not resolve type id 'wallet' as a subtype of `JsonForgotten$Payment`: known type ids = [card, upi] + +$ javac -cp jackson/lib/* jackson/JsonNoSubTypes.java && java JsonNoSubTypes (21.0.10+7-Ubuntu-124.04) +write : {"type":"Card","amount":500} +sealed, Card : Card[amount=500] +sealed, Upi : Upi[amount=500] +sealed, Cheque: InvalidTypeIdException: Could not resolve type id 'Cheque' as a subtype of `JsonNoSubTypes$Payment`: known type ids = [Card, Upi] +plain, OpenCard: InvalidTypeIdException: Could not resolve type id 'OpenCard' as a subtype of `JsonNoSubTypes$OpenPayment`: known type ids = [] + +$ javac -cp jackson/lib/* jackson/JsonNoSubTypes.java && java JsonNoSubTypes (25.0.4.1+1-LTS) +write : {"type":"Card","amount":500} +sealed, Card : Card[amount=500] +sealed, Upi : Upi[amount=500] +sealed, Cheque: InvalidTypeIdException: Could not resolve type id 'Cheque' as a subtype of `JsonNoSubTypes$Payment`: known type ids = [Card, Upi] +plain, OpenCard: InvalidTypeIdException: Could not resolve type id 'OpenCard' as a subtype of `JsonNoSubTypes$OpenPayment`: known type ids = [] + +$ javac -cp jackson/lib/* jackson/JsonNoSubTypes.java && java JsonNoSubTypes (27+35) +write : {"type":"Card","amount":500} +sealed, Card : Card[amount=500] +sealed, Upi : Upi[amount=500] +sealed, Cheque: InvalidTypeIdException: Could not resolve type id 'Cheque' as a subtype of `JsonNoSubTypes$Payment`: known type ids = [Card, Upi] +plain, OpenCard: InvalidTypeIdException: Could not resolve type id 'OpenCard' as a subtype of `JsonNoSubTypes$OpenPayment`: known type ids = [] diff --git a/sealed/run.sh b/sealed/run.sh index 436c54f..2c9ff48 100755 --- a/sealed/run.sh +++ b/sealed/run.sh @@ -56,4 +56,19 @@ mm() { local jdk="$1" o; o="$(mk)" echo "\$ javac -d out \$(find multi/demo.pay -name '*.java') && java -p out -m demo.pay/pay.app.Main ($(t "$jdk"))" "$jdk/bin/javac" -d "$o" $(find multi/demo.pay -name '*.java') 2>&1 && "$jdk/bin/java" -p "$o" -m demo.pay/pay.app.Main 2>&1; } { all3 mm; } > "$OUT/08-modules.txt" + +# 09 - which language level each feature needs (sealed is final in 17; a switch over the sealed type needs 21) +rl() { local jdk="$1" rel="$2" src="$3" o; o="$(mk)" + echo "\$ javac --release $rel $src ($(t "$jdk"))" + "$jdk/bin/javac" --release "$rel" -d "$o" "$src" 2>&1 | grep -v '^Note:'; echo "exit=${PIPESTATUS[0]}"; } +{ rl "$JDK25" 16 src/ImplicitPermits.java; echo; rl "$JDK25" 17 src/ImplicitPermits.java; echo; rl "$JDK25" 17 src/PaymentDemo.java | head -6; } > "$OUT/09-release-levels.txt" + +# 10 - Jackson 3.2.3: does a sealed type help JSON polymorphism? (needs jackson/fetch.sh first; jars are not committed) +jj() { local jdk="$1" src="$2" cls="$3" o cp; o="$(mk)" + cp="jackson/lib/jackson-databind-3.2.3.jar:jackson/lib/jackson-core-3.2.3.jar:jackson/lib/jackson-annotations-2.22.jar" + echo "\$ javac -cp jackson/lib/* $src && java $cls ($(t "$jdk"))" + "$jdk/bin/javac" -cp "$cp" -d "$o" "$src" 2>&1 && "$jdk/bin/java" -cp "$cp:$o" "$cls" 2>&1; } +if [ -f jackson/lib/jackson-databind-3.2.3.jar ]; then + { all3 jj jackson/JsonPayment.java JsonPayment; echo; all3 jj jackson/JsonForgotten.java JsonForgotten; echo; all3 jj jackson/JsonNoSubTypes.java JsonNoSubTypes; } > "$OUT/10-jackson.txt" +else echo "skipped 10-jackson.txt: run jackson/fetch.sh first"; fi echo done