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)); + } +}