diff --git a/pom.xml b/pom.xml
index fbec525b6f..557d0aa1ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -296,6 +296,7 @@
+
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