Skip to content

Commit

Permalink
Support strict Type ID handling.
Browse files Browse the repository at this point in the history
Resolves FasterXML#3853 by
adding support for strict handling of type information when
deserializing into a registered subtype.
  • Loading branch information
stevestorey committed Mar 31, 2023
1 parent 68ba588 commit 66f4bfe
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -58,25 +58,25 @@ 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;
}

/**
* @since 2.15
*/
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
Expand Down Expand Up @@ -203,8 +203,7 @@ protected Object _deserializeTypedUsingDefaultImpl(JsonParser p,
// genuine, or faked for "dont fail on bad type id")
JsonDeserializer<Object> 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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}


0 comments on commit 66f4bfe

Please sign in to comment.