diff --git a/pom.xml b/pom.xml index fbec525b6f..557d0aa1ec 100644 --- a/pom.xml +++ b/pom.xml @@ -296,6 +296,7 @@ src/test-jdk14/java + src/test-jdk17/java diff --git a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java index bed08b655c..f23bd69ad2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java @@ -475,7 +475,7 @@ public VisibilityChecker findAutoDetectVisibility(AnnotatedClass ac, * This includes not only * instantiating resolver builder, but also configuring it based on * relevant annotations (not including ones checked with a call to - * {@link #findSubtypes} + * {@link #findSubtypesByAnnotations} * * @param config Configuration settings in effect (for serialization or deserialization) * @param ac Annotated class to check for annotations @@ -494,7 +494,7 @@ public TypeResolverBuilder findTypeResolver(MapperConfig config, * This includes not only * instantiating resolver builder, but also configuring it based on * relevant annotations (not including ones checked with a call to - * {@link #findSubtypes} + * {@link #findSubtypesByAnnotations} * * @param config Configuration settings in effect (for serialization or deserialization) * @param am Annotated member (field or method) to check for annotations @@ -516,7 +516,7 @@ public TypeResolverBuilder findPropertyTypeResolver(MapperConfig config, * This includes not only * instantiating resolver builder, but also configuring it based on * relevant annotations (not including ones checked with a call to - * {@link #findSubtypes} + * {@link #findSubtypesByAnnotations} * * @param config Configuration settings in effect (for serialization or deserialization) * @param am Annotated member (field or method) to check for annotations @@ -529,6 +529,12 @@ public TypeResolverBuilder findPropertyContentTypeResolver(MapperConfig co AnnotatedMember am, JavaType containerType) { return null; } + + /** + * @deprecated Since 2.14, use {@link #findSubtypesByAnnotations(Annotated)} instead. + */ + @Deprecated // since 2.14 + public List findSubtypes(Annotated a) { return findSubtypesByAnnotations(a); } /** * Method for locating annotation-specified subtypes related to annotated @@ -541,7 +547,20 @@ public TypeResolverBuilder findPropertyContentTypeResolver(MapperConfig co * * @return List of subtype definitions found if any; {@code null} if none */ - public List findSubtypes(Annotated a) { return null; } + public List findSubtypesByAnnotations(Annotated a) { return null; } + + /** + * Method for locating the permitted subclasses specified by a sealed class. + * Note that this is only guaranteed to be a list of direct subtypes, no + * recursive processing is guaranteed (i.e., caller has to do it if/as + * necessary). Note that invoking this method may implicitly load all Jackson + * Java 17 integrations and features. + * + * @param klass A Java content type {@link Class} + * + * @return List of subtype definitions found if any; {@code null} if none + */ + public List findSubtypesByPermittedSubclasses(Class klass) { return null; } /** * Method for checking if specified type has explicit name. diff --git a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java index 5cace6d369..2c589ea96c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java @@ -582,7 +582,15 @@ public enum MapperFeature implements ConfigFeature * * @since 2.13 */ - APPLY_DEFAULT_VALUES(true) + APPLY_DEFAULT_VALUES(true), + + /** + * Feature that determines whether subtypes are discovered from sealed class permitted + * subclasses automatically. + * + * @since 2.14 + */ + DISCOVER_SEALED_CLASS_PERMITTED_SUBCLASSES(true) ; private final boolean _defaultState; diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java index 63d55e64c3..34af1f1797 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java @@ -260,8 +260,27 @@ public TypeResolverBuilder findPropertyContentTypeResolver(MapperConfig co @Override public List findSubtypes(Annotated a) { - List types1 = _primary.findSubtypes(a); - List types2 = _secondary.findSubtypes(a); + return findSubtypesByAnnotations(a); + } + + @Override + public List findSubtypesByAnnotations(Annotated a) + { + List types1 = _primary.findSubtypesByAnnotations(a); + List types2 = _secondary.findSubtypesByAnnotations(a); + if (types1 == null || types1.isEmpty()) return types2; + if (types2 == null || types2.isEmpty()) return types1; + ArrayList result = new ArrayList(types1.size() + types2.size()); + result.addAll(types1); + result.addAll(types2); + return result; + } + + @Override + public List findSubtypesByPermittedSubclasses(Class klass) + { + List types1 = _primary.findSubtypesByPermittedSubclasses(klass); + List types2 = _secondary.findSubtypesByPermittedSubclasses(klass); if (types1 == null || types1.isEmpty()) return types2; if (types2 == null || types2.isEmpty()) return types1; ArrayList result = new ArrayList(types1.size() + types2.size()); diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java index 3f01d57c83..e14cdddeb6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java @@ -3,7 +3,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.*; - +import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.*; @@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.ext.Java7Support; +import com.fasterxml.jackson.databind.jdk17.JDK17Util; import com.fasterxml.jackson.databind.jsontype.NamedType; import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; @@ -614,10 +615,9 @@ public TypeResolverBuilder findPropertyContentTypeResolver(MapperConfig co } return _findTypeResolver(config, am, containerType); } - + @Override - public List findSubtypes(Annotated a) - { + public List findSubtypesByAnnotations(Annotated a) { JsonSubTypes t = _findAnnotation(a, JsonSubTypes.class); if (t == null) return null; JsonSubTypes.Type[] types = t.value(); @@ -667,6 +667,19 @@ private List findSubtypesCheckRepeatedNames(String annotatedTypeName, return result; } + @Override + public List findSubtypesByPermittedSubclasses(Class klass) { + boolean sealed = Optional.ofNullable(JDK17Util.isSealed(klass)).orElse(false); + if (sealed) { + Class[] permittedSubclasses = JDK17Util.getPermittedSubclasses(klass); + if (permittedSubclasses != null && permittedSubclasses.length > 0) { + return Arrays.stream(permittedSubclasses).map(NamedType::new) + .collect(Collectors.toList()); + } + } + return null; + } + @Override public String findTypeName(AnnotatedClass ac) { diff --git a/src/main/java/com/fasterxml/jackson/databind/jdk17/JDK17Util.java b/src/main/java/com/fasterxml/jackson/databind/jdk17/JDK17Util.java new file mode 100644 index 0000000000..946a19dc71 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/jdk17/JDK17Util.java @@ -0,0 +1,85 @@ +package com.fasterxml.jackson.databind.jdk17; + +import java.lang.reflect.Method; +import com.fasterxml.jackson.databind.util.ClassUtil; +import com.fasterxml.jackson.databind.util.NativeImageUtil; + +/** + * Helper class to support some of JDK 17 (and later) features without Jackson itself being run on + * (or even built with) Java 17. In particular allows better support of sealed class types (see + * JEP 409). + * + * @since 2.14 + */ +public class JDK17Util { + public static Boolean isSealed(Class type) { + return SealedClassAccessor.instance().isSealed(type); + } + + public static Class[] getPermittedSubclasses(Class sealedType) { + return SealedClassAccessor.instance().getPermittedSubclasses(sealedType); + } + + static class SealedClassAccessor { + private final Method SEALED_IS_SEALED; + private final Method SEALED_GET_PERMITTED_SUBCLASSES; + + private final static SealedClassAccessor INSTANCE; + private final static RuntimeException PROBLEM; + + static { + RuntimeException prob = null; + SealedClassAccessor inst = null; + try { + inst = new SealedClassAccessor(); + } catch (RuntimeException e) { + prob = e; + } + INSTANCE = inst; + PROBLEM = prob; + } + + private SealedClassAccessor() throws RuntimeException { + try { + SEALED_IS_SEALED = Class.class.getMethod("isSealed"); + SEALED_GET_PERMITTED_SUBCLASSES = Class.class.getMethod("getPermittedSubclasses"); + } catch (Exception e) { + throw new RuntimeException( + String.format("Failed to access Methods needed to support sealed classes: (%s) %s", + e.getClass().getName(), e.getMessage()), + e); + } + } + + public static SealedClassAccessor instance() { + if (PROBLEM != null) { + throw PROBLEM; + } + return INSTANCE; + } + + public Boolean isSealed(Class type) throws IllegalArgumentException { + try { + return (Boolean) SEALED_IS_SEALED.invoke(type); + } catch (Exception e) { + if (NativeImageUtil.isUnsupportedFeatureError(e)) { + return null; + } + throw new IllegalArgumentException( + "Failed to access sealedness of type " + ClassUtil.nameOf(type)); + } + } + + public Class[] getPermittedSubclasses(Class sealedType) throws IllegalArgumentException { + try { + return (Class[]) SEALED_GET_PERMITTED_SUBCLASSES.invoke(sealedType); + } catch (Exception e) { + if (NativeImageUtil.isUnsupportedFeatureError(e)) { + return null; + } + throw new IllegalArgumentException( + "Failed to access permitted subclasses of type " + ClassUtil.nameOf(sealedType)); + } + } + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/jdk17/package-info.java b/src/main/java/com/fasterxml/jackson/databind/jdk17/package-info.java new file mode 100644 index 0000000000..65a44fee53 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/jdk17/package-info.java @@ -0,0 +1,6 @@ +/** +Contains helper class(es) needed to support some of JDK17+ +features without requiring running or building using JDK 17. +*/ + +package com.fasterxml.jackson.databind.jdk17; diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdSubtypeResolver.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdSubtypeResolver.java index e919888288..eac253f93b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdSubtypeResolver.java +++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdSubtypeResolver.java @@ -2,9 +2,9 @@ import java.lang.reflect.Modifier; import java.util.*; - import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.introspect.*; import com.fasterxml.jackson.databind.jsontype.NamedType; @@ -109,7 +109,9 @@ public Collection collectAndResolveSubtypesByClass(MapperConfig co // then annotated types for property itself if (property != null) { - Collection st = ai.findSubtypes(property); + Collection st = ai.findSubtypesByAnnotations(property); + if(st == null && config.isEnabled(MapperFeature.DISCOVER_SEALED_CLASS_PERMITTED_SUBCLASSES)) + st = ai.findSubtypesByPermittedSubclasses(rawBase); if (st != null) { for (NamedType nt : st) { AnnotatedClass ac = AnnotatedClassResolver.resolveWithoutSuperTypes(config, @@ -178,7 +180,9 @@ public Collection collectAndResolveSubtypesByTypeId(MapperConfig c // then with definitions from property if (property != null) { - Collection st = ai.findSubtypes(property); + Collection st = ai.findSubtypesByAnnotations(property); + if(st == null && config.isEnabled(MapperFeature.DISCOVER_SEALED_CLASS_PERMITTED_SUBCLASSES)) + st = ai.findSubtypesByPermittedSubclasses(rawBase); if (st != null) { for (NamedType nt : st) { ac = AnnotatedClassResolver.resolveWithoutSuperTypes(config, nt.getType()); @@ -262,7 +266,9 @@ protected void _collectAndResolve(AnnotatedClass annotatedType, NamedType namedT } // if it wasn't, add and check subtypes recursively collectedSubtypes.put(typeOnlyNamedType, namedType); - Collection st = ai.findSubtypes(annotatedType); + Collection st = ai.findSubtypesByAnnotations(annotatedType); + if(st == null && config.isEnabled(MapperFeature.DISCOVER_SEALED_CLASS_PERMITTED_SUBCLASSES)) + st = ai.findSubtypesByPermittedSubclasses(annotatedType.getRawType()); if (st != null && !st.isEmpty()) { for (NamedType subtype : st) { AnnotatedClass subtypeClass = AnnotatedClassResolver.resolveWithoutSuperTypes(config, @@ -293,7 +299,9 @@ protected void _collectAndResolveByTypeId(AnnotatedClass annotatedType, NamedTyp // only check subtypes if this type hadn't yet been handled if (typesHandled.add(namedType.getType())) { - Collection st = ai.findSubtypes(annotatedType); + Collection st = ai.findSubtypesByAnnotations(annotatedType); + if(st == null && config.isEnabled(MapperFeature.DISCOVER_SEALED_CLASS_PERMITTED_SUBCLASSES)) + st = ai.findSubtypesByPermittedSubclasses(annotatedType.getRawType()); if (st != null && !st.isEmpty()) { for (NamedType subtype : st) { AnnotatedClass subtypeClass = AnnotatedClassResolver.resolveWithoutSuperTypes(config, diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordBasicsTestFailing.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordBasicsTestFailing.java new file mode 100644 index 0000000000..6dad4400e9 --- /dev/null +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordBasicsTestFailing.java @@ -0,0 +1,35 @@ +package com.fasterxml.jackson.databind.failing; + +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.fasterxml.jackson.databind.records.RecordBasicsTest; + +/** + * Tests in this class were moved from {@link RecordBasicsTest}. + */ +public class RecordBasicsTestFailing extends BaseMapTest { + // [databind#2992] + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + record SnakeRecord(String myId, String myValue){} + + private final ObjectMapper MAPPER = newJsonMapper(); + + /* + /********************************************************************** + /* Test methods, naming strategy + /********************************************************************** + */ + + // [databind#2992] + // [databind#3102]: fails on JDK 16 which finally blocks mutation + // of Record fields. + public void testNamingStrategy() throws Exception + { + SnakeRecord input = new SnakeRecord("123", "value"); + String json = MAPPER.writeValueAsString(input); + SnakeRecord output = MAPPER.readValue(json, SnakeRecord.class); + assertEquals(input, output); + } +} diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordUpdate3079TestFailing.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordUpdate3079TestFailing.java new file mode 100644 index 0000000000..3f7178b404 --- /dev/null +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordUpdate3079TestFailing.java @@ -0,0 +1,42 @@ +package com.fasterxml.jackson.databind.failing; + +import java.util.Collections; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.records.RecordUpdate3079Test; + +/** + * Tests in this class were moved from {@link RecordUpdate3079Test}. + */ +public class RecordUpdate3079TestFailing extends BaseMapTest { + record IdNameRecord(int id, String name) { } + + static class IdNameWrapper { + public IdNameRecord value; + + protected IdNameWrapper() { } + public IdNameWrapper(IdNameRecord v) { value = v; } + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + /** + *This test no longer works as of JDK 15 for records. Maybe Unsafe will work? + * + * https://medium.com/@atdsqdjyfkyziqkezu/java-15-breaks-deserialization-of-records-902fcc81253d + * https://stackoverflow.com/questions/61141836/change-static-final-field-in-java-12 + */ + // [databind#3079]: Should be able to Record value directly + // [databind#3102]: fails on JDK 16 which finally blocks mutation + // of Record fields. + public void testDirectRecordUpdate() throws Exception + { + IdNameRecord orig = new IdNameRecord(123, "Bob"); + IdNameRecord result = MAPPER.updateValue(orig, + Collections.singletonMap("id", 137)); + assertNotNull(result); + assertEquals(137, result.id()); + assertEquals("Bob", result.name()); + assertNotSame(orig, result); + } +} diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonNaming3102Test.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonNaming3102Test.java index f554450143..4d6dd0c6f1 100644 --- a/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonNaming3102Test.java +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonNaming3102Test.java @@ -1,15 +1,14 @@ package com.fasterxml.jackson.databind.failing; import com.fasterxml.jackson.annotation.JsonCreator; - import com.fasterxml.jackson.databind.BaseMapTest; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -// [databind#3102]: fails on JDK 16 which finally blocks mutation -// of Record fields. +//[databind#3102]: fails on JDK 16 which finally blocks mutation +//of Record fields. public class RecordWithJsonNaming3102Test extends BaseMapTest { @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonSetter2974Test.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonSetter2974Test.java index 06ee8e75c2..c3f48a7380 100644 --- a/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonSetter2974Test.java +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonSetter2974Test.java @@ -2,7 +2,7 @@ import java.util.List; import java.util.Map; - +import org.junit.Ignore; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.BaseMapTest; @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.exc.InvalidNullException; +@Ignore public class RecordWithJsonSetter2974Test extends BaseMapTest { record RecordWithNonNullDefs(@JsonSetter(nulls=Nulls.AS_EMPTY) List names, diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordBasicsTest.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordBasicsTest.java index 5c6be142b6..e6193c37a4 100644 --- a/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordBasicsTest.java +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordBasicsTest.java @@ -1,12 +1,10 @@ package com.fasterxml.jackson.databind.records; import com.fasterxml.jackson.annotation.*; - +import com.fasterxml.jackson.databind.annotation.*; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.util.ClassUtil; - import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -136,21 +134,6 @@ public void testDeserializeJsonRename() throws Exception { assertEquals(new RecordWithRename(123, "Bob"), value); } - /* - /********************************************************************** - /* Test methods, naming strategy - /********************************************************************** - */ - - // [databind#2992] - public void testNamingStrategy() throws Exception - { - SnakeRecord input = new SnakeRecord("123", "value"); - String json = MAPPER.writeValueAsString(input); - SnakeRecord output = MAPPER.readValue(json, SnakeRecord.class); - assertEquals(input, output); - } - /* /********************************************************************** /* Internal helper methods diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordUpdate3079Test.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordUpdate3079Test.java index 97dd7ce737..9ee4adf094 100644 --- a/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordUpdate3079Test.java +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordUpdate3079Test.java @@ -1,7 +1,5 @@ package com.fasterxml.jackson.databind.records; -import java.util.Collections; - import com.fasterxml.jackson.databind.*; public class RecordUpdate3079Test extends BaseMapTest @@ -17,18 +15,6 @@ protected IdNameWrapper() { } private final ObjectMapper MAPPER = newJsonMapper(); - // [databind#3079]: Should be able to Record value directly - public void testDirectRecordUpdate() throws Exception - { - IdNameRecord orig = new IdNameRecord(123, "Bob"); - IdNameRecord result = MAPPER.updateValue(orig, - Collections.singletonMap("id", 137)); - assertNotNull(result); - assertEquals(137, result.id()); - assertEquals("Bob", result.name()); - assertNotSame(orig, result); - } - // [databind#3079]: also: should be able to Record valued property public void testRecordAsPropertyUpdate() throws Exception { diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/jdk17/SealedBasicsTest.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/jdk17/SealedBasicsTest.java new file mode 100644 index 0000000000..e8e913b5e5 --- /dev/null +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/jdk17/SealedBasicsTest.java @@ -0,0 +1,444 @@ +package com.fasterxml.jackson.databind.jdk17; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import java.io.IOException; +import java.util.Objects; +import org.junit.Test; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; + +public class SealedBasicsTest { + /** + * Our {@link ObjectMapper} uses the default configuration that explicitly enables the sealed + * classes subtype discovery. + */ + @SuppressWarnings("deprecation") + public static final ObjectMapper MAPPER = + new ObjectMapper().configure(MapperFeature.DISCOVER_SEALED_CLASS_PERMITTED_SUBCLASSES, true); + + /** + * The "ExampleOne" objects test serialization of sealed classes without a JsonSubTypes + * annotation. + */ + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + public static sealed class ExampleOne permits AlphaExampleOne, BravoExampleOne {} + + /** + * Provide an explicit JsonTypeName here + */ + @JsonTypeName("alpha") + public static final class AlphaExampleOne extends ExampleOne { + private String alpha; + + public AlphaExampleOne() {} + + public AlphaExampleOne(String alpha) { + this.alpha = alpha; + } + + /** + * @return the alpha + */ + public String getAlpha() { + return alpha; + } + + /** + * @param alpha the alpha to set + */ + public void setAlpha(String alpha) { + this.alpha = alpha; + } + + @Override + public int hashCode() { + return Objects.hash(alpha); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AlphaExampleOne other = (AlphaExampleOne) obj; + return Objects.equals(alpha, other.alpha); + } + } + + /** + * Use the default type name here + */ + public static final class BravoExampleOne extends ExampleOne { + public String bravo; + + public BravoExampleOne() {} + + public BravoExampleOne(String bravo) { + this.bravo = bravo; + } + + /** + * @return the bravo + */ + public String getBravo() { + return bravo; + } + + /** + * @param bravo the bravo to set + */ + public void setBravo(String bravo) { + this.bravo = bravo; + } + + @Override + public int hashCode() { + return Objects.hash(bravo); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + BravoExampleOne other = (BravoExampleOne) obj; + return Objects.equals(bravo, other.bravo); + } + } + + @Test + public void oneAlphaSerializationTest() throws IOException { + final String alpha = "apple"; + final String serialized = MAPPER.writeValueAsString(new AlphaExampleOne(alpha)); + assertThat(serialized, is("{\"type\":\"alpha\",\"alpha\":\"" + alpha + "\"}")); + } + + @Test + public void oneAlphaDeserializationTest() throws IOException { + final String alpha = "apple"; + ExampleOne one = + MAPPER.readValue("{\"type\":\"alpha\",\"alpha\":\"" + alpha + "\"}", ExampleOne.class); + assertThat(one, is(new AlphaExampleOne(alpha))); + } + + @Test + public void oneBravoDeserializationTest() throws IOException { + final String bravo = "blueberry"; + ExampleOne one = MAPPER.readValue( + "{\"type\":\"SealedBasicsTest$BravoExampleOne\",\"bravo\":\"" + bravo + "\"}", + ExampleOne.class); + assertThat(one, is(new BravoExampleOne(bravo))); + } + + @Test + public void oneBravoSerializationTest() throws IOException { + final String bravo = "blueberry"; + final String serialized = MAPPER.writeValueAsString(new BravoExampleOne(bravo)); + assertThat(serialized, + is("{\"type\":\"SealedBasicsTest$BravoExampleOne\",\"bravo\":\"" + bravo + "\"}")); + } + + /** + * Jackson is quite smart and still picks up the supertype relationship during serialization, even + * without automatic subtype discovery. + */ + @Test + @SuppressWarnings("deprecation") + public void oneAlphaSerializationTestDiscoveryDisabled() throws IOException { + final String bravo = "blueberry"; + final String serialized = + new ObjectMapper().configure(MapperFeature.DISCOVER_SEALED_CLASS_PERMITTED_SUBCLASSES, false) + .writeValueAsString(new BravoExampleOne(bravo)); + assertThat(serialized, + is("{\"type\":\"SealedBasicsTest$BravoExampleOne\",\"bravo\":\"" + bravo + "\"}")); + } + + /** + * Jackson should not pick up the subtype relationship without automatic discovery. + */ + @SuppressWarnings("deprecation") + @Test(expected = InvalidTypeIdException.class) + public void oneAlphaDeserializationTestDiscoveryDisabled() throws IOException { + new ObjectMapper().configure(MapperFeature.DISCOVER_SEALED_CLASS_PERMITTED_SUBCLASSES, false) + .readValue("{\"type\":\"alpha\",\"alpha\":\"apple\"}", ExampleOne.class); + } + + /** + * The "ExampleTwo" objects test serialization of sealed classes with a JsonSubTypes annotation, + * which is the existing approach that must not break. + */ + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(AlphaExampleTwo.class), + @JsonSubTypes.Type(value = BravoExampleTwo.class, name = "bravo") + }) + public static sealed class ExampleTwo permits AlphaExampleTwo, BravoExampleTwo {} + + /** + * Provide an explicit JsonTypeName here + */ + @JsonTypeName("alpha") + public static final class AlphaExampleTwo extends ExampleTwo { + private String alpha; + + public AlphaExampleTwo() {} + + public AlphaExampleTwo(String alpha) { + this.alpha = alpha; + } + + /** + * @return the alpha + */ + public String getAlpha() { + return alpha; + } + + /** + * @param alpha the alpha to set + */ + public void setAlpha(String alpha) { + this.alpha = alpha; + } + + @Override + public int hashCode() { + return Objects.hash(alpha); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AlphaExampleTwo other = (AlphaExampleTwo) obj; + return Objects.equals(alpha, other.alpha); + } + } + + + /** + * Use the default type name here + */ + public static final class BravoExampleTwo extends ExampleTwo { + public String bravo; + + public BravoExampleTwo() {} + + public BravoExampleTwo(String bravo) { + this.bravo = bravo; + } + + /** + * @return the bravo + */ + public String getBravo() { + return bravo; + } + + /** + * @param bravo the bravo to set + */ + public void setBravo(String bravo) { + this.bravo = bravo; + } + + @Override + public int hashCode() { + return Objects.hash(bravo); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + BravoExampleTwo other = (BravoExampleTwo) obj; + return Objects.equals(bravo, other.bravo); + } + + } + + @Test + public void twoAlphaSerializationTest() throws IOException { + final String alpha = "apple"; + final String serialized = MAPPER.writeValueAsString(new AlphaExampleTwo(alpha)); + assertThat(serialized, is("{\"type\":\"alpha\",\"alpha\":\"" + alpha + "\"}")); + } + + @Test + public void twoAlphaDeserializationTest() throws IOException { + final String alpha = "apple"; + ExampleTwo two = + MAPPER.readValue("{\"type\":\"alpha\",\"alpha\":\"" + alpha + "\"}", ExampleTwo.class); + assertThat(two, is(new AlphaExampleTwo(alpha))); + } + + @Test + public void twoBravoDeserializationTest() throws IOException { + final String bravo = "blueberry"; + // Make sure we pick up the "bravo" name from the @@JsonSubTypes annotation. + ExampleTwo two = + MAPPER.readValue("{\"type\":\"bravo\",\"bravo\":\"" + bravo + "\"}", ExampleTwo.class); + assertThat(two, is(new BravoExampleTwo(bravo))); + } + + @Test + public void twoBravoSerializationTest() throws IOException { + final String bravo = "blueberry"; + final String serialized = MAPPER.writeValueAsString(new BravoExampleTwo(bravo)); + // Make sure we pick up the "bravo" name from the @@JsonSubTypes annotation. + assertThat(serialized, is("{\"type\":\"bravo\",\"bravo\":\"" + bravo + "\"}")); + } + + /** + * The "ExampleTwo" objects test serialization of conventional classes with a JsonSubTypes + * annotation, which is the existing approach that must not break. + */ + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(AlphaExampleThree.class), + @JsonSubTypes.Type(value = BravoExampleThree.class, name = "bravo") + }) + public static class ExampleThree {} + + /** + * Provide an explicit JsonTypeName here + */ + @JsonTypeName("alpha") + public static final class AlphaExampleThree extends ExampleThree { + private String alpha; + + public AlphaExampleThree() {} + + public AlphaExampleThree(String alpha) { + this.alpha = alpha; + } + + /** + * @return the alpha + */ + public String getAlpha() { + return alpha; + } + + /** + * @param alpha the alpha to set + */ + public void setAlpha(String alpha) { + this.alpha = alpha; + } + + @Override + public int hashCode() { + return Objects.hash(alpha); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AlphaExampleThree other = (AlphaExampleThree) obj; + return Objects.equals(alpha, other.alpha); + } + } + + + /** + * Use the default type name here + */ + public static final class BravoExampleThree extends ExampleThree { + public String bravo; + + public BravoExampleThree() {} + + public BravoExampleThree(String bravo) { + this.bravo = bravo; + } + + /** + * @return the bravo + */ + public String getBravo() { + return bravo; + } + + /** + * @param bravo the bravo to set + */ + public void setBravo(String bravo) { + this.bravo = bravo; + } + + @Override + public int hashCode() { + return Objects.hash(bravo); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + BravoExampleThree other = (BravoExampleThree) obj; + return Objects.equals(bravo, other.bravo); + } + + } + + @Test + public void threeAlphaSerializationTest() throws IOException { + final String alpha = "apple"; + final String serialized = MAPPER.writeValueAsString(new AlphaExampleThree(alpha)); + assertThat(serialized, is("{\"type\":\"alpha\",\"alpha\":\"" + alpha + "\"}")); + } + + @Test + public void threeAlphaDeserializationTest() throws IOException { + final String alpha = "apple"; + ExampleThree three = + MAPPER.readValue("{\"type\":\"alpha\",\"alpha\":\"" + alpha + "\"}", ExampleThree.class); + assertThat(three, is(new AlphaExampleThree(alpha))); + } + + @Test + public void threeBravoDeserializationTest() throws IOException { + final String bravo = "blueberry"; + // Make sure we pick up the "bravo" name from the @@JsonSubTypes annotation. + ExampleThree three = + MAPPER.readValue("{\"type\":\"bravo\",\"bravo\":\"" + bravo + "\"}", ExampleThree.class); + assertThat(three, is(new BravoExampleThree(bravo))); + } + + @Test + public void threeBravoSerializationTest() throws IOException { + final String bravo = "blueberry"; + final String serialized = MAPPER.writeValueAsString(new BravoExampleThree(bravo)); + // Make sure we pick up the "bravo" name from the @@JsonSubTypes annotation. + assertThat(serialized, is("{\"type\":\"bravo\",\"bravo\":\"" + bravo + "\"}")); + } +} \ No newline at end of file