diff --git a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java index c48ff1df26f..0181d6c1ca9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java @@ -328,6 +328,15 @@ public enum MapperFeature implements ConfigFeature */ INFER_BUILDER_TYPE_BINDINGS(true), + /** + * Feature that determines what happens when deserializing to a registered sub-type, but no + * type information has been provided. If enabled, then an {@link InvalidTypeIdException} + * will be thrown, if disabled then the deserialization will proceed without the type information. + * + * @since 2.15 + */ + STRICT_TYPE_ID_HANDLING(false), + /* /****************************************************** /* View-related features diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java index 3dbc4a415ff..9e9712adc62 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsPropertyTypeDeserializer.java @@ -29,11 +29,11 @@ public class AsPropertyTypeDeserializer extends AsArrayTypeDeserializer protected final As _inclusion; /** - * Indicates if the current class has a TypeResolver attached or not. + * Indicates that we should use the base type during deserialization if the type information is missing. * * @since 2.15 */ - protected final boolean _hasTypeResolver; + protected final boolean _strictTypeIdHandling; // @since 2.12.2 (see [databind#3055] protected final String _msgForMissingId = (_property == null) @@ -58,13 +58,13 @@ public AsPropertyTypeDeserializer(JavaType bt, TypeIdResolver idRes, { super(bt, idRes, typePropertyName, typeIdVisible, defaultImpl); _inclusion = inclusion; - _hasTypeResolver = true; + _strictTypeIdHandling = false; } public AsPropertyTypeDeserializer(AsPropertyTypeDeserializer src, BeanProperty property) { super(src, property); _inclusion = src._inclusion; - _hasTypeResolver = src._hasTypeResolver; + _strictTypeIdHandling = src._strictTypeIdHandling; } /** @@ -72,11 +72,11 @@ public AsPropertyTypeDeserializer(AsPropertyTypeDeserializer src, BeanProperty p */ public AsPropertyTypeDeserializer(JavaType bt, TypeIdResolver idRes, String typePropertyName, boolean typeIdVisible, JavaType defaultImpl, - As inclusion, boolean hasTypeResolver) + As inclusion, boolean strictTypeIdHandling) { super(bt, idRes, typePropertyName, typeIdVisible, defaultImpl); _inclusion = inclusion; - _hasTypeResolver = hasTypeResolver; + _strictTypeIdHandling = strictTypeIdHandling; } @Override @@ -203,8 +203,7 @@ protected Object _deserializeTypedUsingDefaultImpl(JsonParser p, // genuine, or faked for "dont fail on bad type id") JsonDeserializer deser = _findDefaultImplDeserializer(ctxt); if (deser == null) { - JavaType t = _hasTypeResolver - ? _handleMissingTypeId(ctxt, priorFailureMsg) : _baseType; + JavaType t = _strictTypeIdHandling ? _handleMissingTypeId(ctxt, priorFailureMsg): _baseType; if (t == null) { // 09-Mar-2017, tatu: Is this the right thing to do? 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 71fab07fe33..03de4134b1c 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 @@ -177,7 +177,7 @@ public TypeDeserializer buildTypeDeserializer(DeserializationConfig config, case EXISTING_PROPERTY: // as per [#528] same class as PROPERTY return new AsPropertyTypeDeserializer(baseType, idRes, _typeProperty, _typeIdVisible, defaultImpl, _includeAs, - _hasTypeResolver(config, baseType)); + _strictTypeIdHandling(config, baseType)); case WRAPPER_OBJECT: return new AsWrapperTypeDeserializer(baseType, idRes, _typeProperty, _typeIdVisible, defaultImpl); @@ -404,18 +404,23 @@ protected boolean allowPrimitiveTypes(MapperConfig config, } /** - * Checks whether the given class has annotations indicating some type resolver - * is applied, for example {@link com.fasterxml.jackson.annotation.JsonSubTypes}. - * Only initializes {@link #_hasTypeResolver} once if its value is null. + * Determines whether strict type ID handling should be used for this type or not. + * This will be enabled when either the type has type resolver annotations or if + * {@link com.fasterxml.jackson.databind.DeserializationFeature#FAIL_ON_MISSING_TYPE_NAME} + * is enabled. * * @param config the deserialization configuration to use * @param baseType the base type to check for type resolver annotations * - * @return true if the class has type resolver annotations, false otherwise + * @return {@code true} if the class has type resolver annotations, or the strict + * handling feature is enabled, {@code false} otherwise. * * @since 2.15 */ - protected boolean _hasTypeResolver(DeserializationConfig config, JavaType baseType) { + protected boolean _strictTypeIdHandling(DeserializationConfig config, JavaType baseType) { + if (config.isEnabled(MapperFeature.STRICT_TYPE_ID_HANDLING)) { + return true; + } AnnotatedClass ac = AnnotatedClassResolver.resolveWithoutSuperTypes(config, baseType.getRawClass()); AnnotationIntrospector ai = config.getAnnotationIntrospector(); return ai.findTypeResolver(config, ac, baseType) != null; diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/StrictJsonTypeInfoHandlingTest.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/StrictJsonTypeInfoHandlingTest.java new file mode 100644 index 00000000000..dd4a5ed7898 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/StrictJsonTypeInfoHandlingTest.java @@ -0,0 +1,94 @@ +package com.fasterxml.jackson.databind.jsontype; + + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import com.fasterxml.jackson.databind.json. JsonMapper; + +public class StrictJsonTypeInfoHandlingTest extends BaseMapTest { + + @JsonTypeInfo(use = Id.NAME) + interface Command { + } + + @JsonTypeName("do-something") + static class DoSomethingCommand implements Command { + } + + public void testDefaultNonStrictTypeHandling() throws Exception { + ObjectMapper om = new ObjectMapper(); + om.registerSubtypes(DoSomethingCommand.class); + + // This should pass in all scenarios + verifyDeserializationWithFullTypeInfo(om); + // and throw an exception if the target was a super-type in all cases + verifyInvalidTypeIdWithSuperclassTarget(om); + + // Default is to allow the deserialization without a type if the target is a concrete sub-type + verifyDeserializationWithConcreteTarget(om); + } + + public void testExplicitNonStrictTypeHandling() throws Exception { + ObjectMapper om = JsonMapper.builder().disable(MapperFeature.STRICT_TYPE_ID_HANDLING).build(); + om.registerSubtypes(DoSomethingCommand.class); + + // This should pass in all scenarios + verifyDeserializationWithFullTypeInfo(om); + // and throw an exception if the target was a super-type in all cases + verifyInvalidTypeIdWithSuperclassTarget(om); + + // Default is to allow the deserialization without a type if the target is a concrete sub-type + verifyDeserializationWithConcreteTarget(om); + } + + public void testStrictTypeHandling() throws Exception { + ObjectMapper om = JsonMapper.builder().enable(MapperFeature.STRICT_TYPE_ID_HANDLING).build(); + om.registerSubtypes(DoSomethingCommand.class); + + // This should pass in all scenarios + verifyDeserializationWithFullTypeInfo(om); + // and throw an exception if the target was a super-type in all cases + verifyInvalidTypeIdWithSuperclassTarget(om); + + // With strict mode enabled, fail if there's no type information on the JSON + verifyInvalidTypeIdWithConcreteTarget(om); + + } + + private void verifyInvalidTypeIdWithSuperclassTarget(ObjectMapper om) throws Exception { + try { + om.readValue("{}", Command.class); + fail("Should not pass"); + } catch (InvalidTypeIdException e) { + verifyException(e, "missing type id property '@type'"); + } + } + + private void verifyInvalidTypeIdWithConcreteTarget(ObjectMapper om) throws Exception { + try { + om.readValue("{}", DoSomethingCommand.class); + fail("Should not pass"); + } catch (InvalidTypeIdException e) { + verifyException(e, "missing type id property '@type'"); + } + } + + private void verifyDeserializationWithConcreteTarget(ObjectMapper om) throws Exception { + DoSomethingCommand cmd = om.readValue("{}", DoSomethingCommand.class); + assertType(cmd, DoSomethingCommand.class); + } + + private void verifyDeserializationWithFullTypeInfo(ObjectMapper om) throws Exception { + Command cmd = om.readValue("{\"@type\":\"do-something\"}", Command.class); + assertType(cmd, DoSomethingCommand.class); + cmd = om.readValue("{\"@type\":\"do-something\"}", DoSomethingCommand.class); + assertType(cmd, DoSomethingCommand.class); + } +} + +