diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 079e2cd69d..013cd88b7d 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -76,6 +76,8 @@ Project: jackson-databind
failure of `java.util.Optional` (de)serialization without Java 8 module
#5014: Add `java.lang.Runnable` as unsafe base type in `DefaultBaseTypeLimitingValidator`
#5020: Support new `@JsonProperty.isRequired` for overridable definition of "required-ness"
+#5027: Add `DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED`
+ (contributed by @pjfanning)
#5052: Minor bug in `FirstCharBasedValidator.forFirstNameRule()`: returns `null`
in non-default case
diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java
index a1f2837fbc..4918e41173 100644
--- a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java
+++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java
@@ -253,7 +253,7 @@ public enum DeserializationFeature implements ConfigFeature
FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY(true),
/**
- * Feature that determines behaviour for data-binding after binding the root value.
+ * Feature that determines behavior for data-binding after binding the root value.
* If feature is enabled, one more call to
* {@link com.fasterxml.jackson.core.JsonParser#nextToken} is made to ensure that
* no more tokens are found (and if any is found,
@@ -272,6 +272,24 @@ public enum DeserializationFeature implements ConfigFeature
*/
FAIL_ON_TRAILING_TOKENS(false),
+ /**
+ * Feature that determines behavior when deserializing polymorphic types that use
+ * Class-based Type Id mechanism (either
+ * {@code JsonTypeInfo.Id.CLASS} or {@code JsonTypeInfo.Id.MINIMAL_CLASS}):
+ * If enabled, an exception will be
+ * thrown if a subtype (Class) is encountered that has not been explicitly registered (by
+ * calling {@link ObjectMapper#registerSubtypes} or
+ * {@link com.fasterxml.jackson.annotation.JsonSubTypes}).
+ *
+ * Note that for Type Name - based Type Id mechanism ({@code JsonTypeInfo.Id.NAME})
+ * you already need to register the subtypes but with so this feature has no effect.
+ *
+ * Feature is disabled by default.
+ *
+ * @since 2.19
+ */
+ FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED(false),
+
/**
* Feature that determines whether Jackson code should catch
* and wrap {@link Exception}s (but never {@link Error}s!)
diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/ClassNameIdResolver.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/ClassNameIdResolver.java
index 9952a00640..42cd3a32b7 100644
--- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/ClassNameIdResolver.java
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/ClassNameIdResolver.java
@@ -7,6 +7,7 @@
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.ClassUtil;
@@ -26,6 +27,11 @@ public class ClassNameIdResolver
protected final PolymorphicTypeValidator _subTypeValidator;
+ /**
+ * @since 2.19 (to support {@code DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED})
+ */
+ protected final Set _allowedSubtypes;
+
/**
* @deprecated Since 2.10 use variant that takes {@link PolymorphicTypeValidator}
*/
@@ -36,21 +42,56 @@ protected ClassNameIdResolver(JavaType baseType, TypeFactory typeFactory) {
/**
* @since 2.10
+ * @deprecated Since 2.19 use variant that takes {@code Collection}
*/
+ @Deprecated
public ClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
PolymorphicTypeValidator ptv) {
+ this(baseType, typeFactory, null, ptv);
+ }
+
+ /**
+ * @since 2.19
+ */
+ public ClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
+ Collection subtypes, PolymorphicTypeValidator ptv) {
super(baseType, typeFactory);
_subTypeValidator = ptv;
+ Set allowedSubtypes = null;
+ if (subtypes != null) {
+ for (NamedType t : subtypes) {
+ if (allowedSubtypes == null) {
+ allowedSubtypes = new HashSet<>();
+ }
+ allowedSubtypes.add(t.getType().getName());
+ }
+ }
+ _allowedSubtypes = (allowedSubtypes == null) ? Collections.emptySet() : allowedSubtypes;
}
+ /**
+ * @deprecated since 2.19
+ */
+ @Deprecated
+ public static ClassNameIdResolver construct(JavaType baseType,
+ MapperConfig> config, PolymorphicTypeValidator ptv) {
+ return new ClassNameIdResolver(baseType, config.getTypeFactory(), ptv);
+ }
+
+ /**
+ * @since 2.19
+ */
public static ClassNameIdResolver construct(JavaType baseType, MapperConfig> config,
+ Collection subtypes,
PolymorphicTypeValidator ptv) {
- return new ClassNameIdResolver(baseType, config.getTypeFactory(), ptv);
+ return new ClassNameIdResolver(baseType, config.getTypeFactory(), subtypes, ptv);
}
@Override
public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.CLASS; }
+ // 28-Mar-2025, tatu: Why is this here; not overridden so... ?
+ @Deprecated // since 2.19
public void registerSubtype(Class> type, String name) {
// not used with class name - based resolvers
}
@@ -72,14 +113,21 @@ public JavaType typeFromId(DatabindContext context, String id) throws IOExceptio
protected JavaType _typeFromId(String id, DatabindContext ctxt) throws IOException
{
- // 24-Apr-2019, tatu: [databind#2195] validate as well as resolve:
- JavaType t = ctxt.resolveAndValidateSubType(_baseType, id, _subTypeValidator);
- if (t == null) {
- if (ctxt instanceof DeserializationContext) {
- // First: we may have problem handlers that can deal with it?
- return ((DeserializationContext) ctxt).handleUnknownTypeId(_baseType, id, this, "no such class found");
+ DeserializationContext deserializationContext = null;
+ if (ctxt instanceof DeserializationContext) {
+ deserializationContext = (DeserializationContext) ctxt;
+ }
+ if ((_allowedSubtypes != null) && (deserializationContext != null)
+ && deserializationContext.isEnabled(
+ DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED)) {
+ if (!_allowedSubtypes.contains(id)) {
+ throw deserializationContext.invalidTypeIdException(_baseType, id,
+"`DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED` is enabled and the input class is not registered using `@JsonSubTypes` annotation");
}
- // ... meaning that we really should never get here.
+ }
+ final JavaType t = ctxt.resolveAndValidateSubType(_baseType, id, _subTypeValidator);
+ if (t == null && deserializationContext != null) {
+ return deserializationContext.handleUnknownTypeId(_baseType, id, this, "no such class found");
}
return t;
}
diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/MinimalClassNameIdResolver.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/MinimalClassNameIdResolver.java
index c4f08b07b0..9db48c8894 100644
--- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/MinimalClassNameIdResolver.java
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/MinimalClassNameIdResolver.java
@@ -1,12 +1,14 @@
package com.fasterxml.jackson.databind.jsontype.impl;
import java.io.IOException;
+import java.util.Collection;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.type.TypeFactory;
@@ -32,10 +34,24 @@ public class MinimalClassNameIdResolver
*/
protected final String _basePackagePrefix;
+ /**
+ * @deprecated since 2.19
+ */
+ @Deprecated
+ protected MinimalClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
+ PolymorphicTypeValidator ptv)
+ {
+ this(baseType, typeFactory, null, ptv);
+ }
+
+ /**
+ * @since 2.19
+ */
protected MinimalClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
+ Collection subtypes,
PolymorphicTypeValidator ptv)
{
- super(baseType, typeFactory, ptv);
+ super(baseType, typeFactory, subtypes, ptv);
String base = baseType.getRawClass().getName();
int ix = base.lastIndexOf('.');
if (ix < 0) { // can this ever occur?
@@ -47,11 +63,24 @@ protected MinimalClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
}
}
+ /**
+ * @deprecated since 2.19
+ */
+ @Deprecated
public static MinimalClassNameIdResolver construct(JavaType baseType, MapperConfig> config,
PolymorphicTypeValidator ptv) {
return new MinimalClassNameIdResolver(baseType, config.getTypeFactory(), ptv);
}
+ /**
+ * @since 2.19
+ */
+ public static MinimalClassNameIdResolver construct(JavaType baseType, MapperConfig> config,
+ Collection subtypes,
+ PolymorphicTypeValidator ptv) {
+ return new MinimalClassNameIdResolver(baseType, config.getTypeFactory(), subtypes, ptv);
+ }
+
@Override
public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.MINIMAL_CLASS; }
diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
index 988f39a165..a64f13da74 100644
--- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
@@ -356,9 +356,9 @@ protected TypeIdResolver idResolver(MapperConfig> config,
switch (_idType) {
case DEDUCTION: // Deduction produces class names to be resolved
case CLASS:
- return ClassNameIdResolver.construct(baseType, config, subtypeValidator);
+ return ClassNameIdResolver.construct(baseType, config, subtypes, subtypeValidator);
case MINIMAL_CLASS:
- return MinimalClassNameIdResolver.construct(baseType, config, subtypeValidator);
+ return MinimalClassNameIdResolver.construct(baseType, config, subtypes, subtypeValidator);
case SIMPLE_NAME:
return SimpleNameIdResolver.construct(config, baseType, subtypes, forSer, forDeser);
case NAME:
diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/RegisteredClassDeser5027Test.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/RegisteredClassDeser5027Test.java
new file mode 100644
index 0000000000..f98d331090
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/RegisteredClassDeser5027Test.java
@@ -0,0 +1,85 @@
+package com.fasterxml.jackson.databind.jsontype;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
+import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+// For [databind#5027]
+public class RegisteredClassDeser5027Test extends DatabindTestUtil
+{
+ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+ @JsonSubTypes({@JsonSubTypes.Type(value = FooClassImpl.class)})
+ static abstract class FooClass { }
+ static class FooClassImpl extends FooClass { }
+ static class FooClassImpl2 extends FooClass { }
+
+ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+ static abstract class FooClassNoRegSubTypes { }
+ static class FooClassNoRegSubTypesImpl extends FooClassNoRegSubTypes { }
+
+ @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
+ @JsonSubTypes({@JsonSubTypes.Type(value = FooMinClassImpl.class)})
+ static abstract class FooMinClass { }
+ static class FooMinClassImpl extends FooMinClass { }
+ static class FooMinClassImpl2 extends FooMinClass { }
+
+ /*
+ /************************************************************
+ /* Unit tests, valid
+ /************************************************************
+ */
+
+ private final ObjectMapper MAPPER = jsonMapperBuilder()
+ .enable(DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED)
+ .build();
+
+ @Test
+ public void testDeserializationIdClass() throws Exception
+ {
+ //trying to test if JsonSubTypes enforced
+ final String foo1 = MAPPER.writeValueAsString(new FooClassImpl());
+ final String foo2 = MAPPER.writeValueAsString(new FooClassImpl2());
+ FooClass res1 = MAPPER.readValue(foo1, FooClass.class);
+ assertTrue(res1 instanceof FooClassImpl);
+ // next bit should fail because FooClassImpl2 is not listed as a subtype (see mapper config)
+ assertThrows(InvalidTypeIdException.class, () -> MAPPER.readValue(foo2, FooClass.class));
+ }
+
+ @Test
+ public void testDeserializationIdClassNoReg() throws Exception
+ {
+ final ObjectMapper mapper = newJsonMapper();
+ final String foo1 = mapper.writeValueAsString(new FooClassNoRegSubTypesImpl());
+ // the default mapper should be able to deserialize the object (sub type check not enforced)
+ FooClassNoRegSubTypes res1 = mapper.readValue(foo1, FooClassNoRegSubTypes.class);
+ assertTrue(res1 instanceof FooClassNoRegSubTypesImpl);
+ }
+
+ @Test
+ public void testDefaultDeserializationIdClassNoReg() throws Exception
+ {
+ //trying to test if JsonSubTypes enforced
+ final String foo1 = MAPPER.writeValueAsString(new FooClassNoRegSubTypesImpl());
+ // next bit should fail because FooClassImpl2 is not listed as a subtype (see mapper config)
+ assertThrows(InvalidTypeIdException.class, () -> MAPPER.readValue(foo1, FooClassNoRegSubTypes.class));
+ }
+
+ @Test
+ public void testDeserializationIdMinimalClass() throws Exception
+ {
+ //trying to test if JsonSubTypes enforced
+ final String foo1 = MAPPER.writeValueAsString(new FooMinClassImpl());
+ final String foo2 = MAPPER.writeValueAsString(new FooMinClassImpl2());
+ FooMinClass res1 = MAPPER.readValue(foo1, FooMinClass.class);
+ assertTrue(res1 instanceof FooMinClassImpl);
+ // next bit should fail because FooMinClassImpl2 is not listed as a subtype (see mapper config)
+ assertThrows(InvalidTypeIdException.class, () -> MAPPER.readValue(foo2, FooMinClass.class));
+ }
+}