From c5eabf8b96018e2563934776a5d82da0371f7b62 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Tue, 6 Jun 2023 22:35:16 +0900 Subject: [PATCH] Add new `@JsonTypeInfo.requireTypeIdForSubtypes` usage (#3891) Implements #3877 --- .../jsontype/impl/StdTypeResolverBuilder.java | 14 ++- ...verrideStrictTypeInfoHandling3877Test.java | 116 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/jsontype/OverrideStrictTypeInfoHandling3877Test.java 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 32da2e1795..cfc8fb058f 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 @@ -37,6 +37,11 @@ public class StdTypeResolverBuilder protected boolean _typeIdVisible = false; /** + * + * Boolean value configured through {@link JsonTypeInfo#requireTypeIdForSubtypes}. + * If this value is not {@code null}, this value should override the global configuration of + * {@link com.fasterxml.jackson.databind.MapperFeature#REQUIRE_TYPE_ID_FOR_SUBTYPES}. + * * @since 2.16 (backported from Jackson 3.0) */ protected Boolean _requireTypeIdForSubtypes; @@ -466,7 +471,10 @@ protected boolean allowPrimitiveTypes(MapperConfig config, /** * 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 + * This will be enabld as configured by {@link JsonTypeInfo#requireTypeIdForSubtypes()} + * unless its value is {@link com.fasterxml.jackson.annotation.OptBoolean#DEFAULT}. + * In case the value of {@link JsonTypeInfo#requireTypeIdForSubtypes()} is {@code OptBoolean.DEFAULT}, + * this will be enabled when either the type has type resolver annotations or if * {@link com.fasterxml.jackson.databind.MapperFeature#REQUIRE_TYPE_ID_FOR_SUBTYPES} * is enabled. * @@ -479,6 +487,10 @@ protected boolean allowPrimitiveTypes(MapperConfig config, * @since 2.15 */ protected boolean _strictTypeIdHandling(DeserializationConfig config, JavaType baseType) { + // [databind#3877]: per-type strict type handling, since 2.16 + if (_requireTypeIdForSubtypes != null && baseType.isConcrete()) { + return _requireTypeIdForSubtypes; + } if (config.isEnabled(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES)) { return true; } diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/OverrideStrictTypeInfoHandling3877Test.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/OverrideStrictTypeInfoHandling3877Test.java new file mode 100644 index 0000000000..b54c503c6c --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/OverrideStrictTypeInfoHandling3877Test.java @@ -0,0 +1,116 @@ +package com.fasterxml.jackson.databind.jsontype; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.annotation.OptBoolean; +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; + +// [databind#3877]: allow configuration of per-type strict type handling +public class OverrideStrictTypeInfoHandling3877Test extends BaseMapTest { + + /* + /********************************************************** + /* Set Up + /********************************************************** + */ + + @JsonTypeInfo(use = Id.NAME, requireTypeIdForSubtypes = OptBoolean.DEFAULT) + @JsonSubTypes({ + @JsonSubTypes.Type(value = DoDefaultCommand.class, name = "do-default")}) + interface DefaultCommand {} + + static class DoDefaultCommand implements DefaultCommand {} + + @JsonTypeInfo(use = Id.NAME, requireTypeIdForSubtypes = OptBoolean.TRUE) + @JsonSubTypes({ + @JsonSubTypes.Type(value = DoTrueCommand.class, name = "do-true")}) + interface TrueCommand {} + + static class DoTrueCommand implements TrueCommand {} + + @JsonTypeInfo(use = Id.NAME, requireTypeIdForSubtypes = OptBoolean.FALSE) + @JsonSubTypes({ + @JsonSubTypes.Type(value = DoFalseCommand.class, name = "do-false")}) + interface FalseCommand {} + + static class DoFalseCommand implements FalseCommand {} + + /* + /********************************************************** + /* Tests + /********************************************************** + */ + + private final ObjectMapper ENABLED_MAPPER = JsonMapper.builder().enable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES).build(); + private final ObjectMapper DISABLED_MAPPER = JsonMapper.builder().disable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES).build(); + private final ObjectMapper DEFAULT_MAPPER = JsonMapper.builder().build(); + + public void testMissingTypeId() throws Exception { + // super types fail on missing-id no matter what + verifyFailureMissingTypeId("{}", FalseCommand.class, ENABLED_MAPPER); + verifyFailureMissingTypeId("{}", FalseCommand.class, DEFAULT_MAPPER); + verifyFailureMissingTypeId("{}", FalseCommand.class, DISABLED_MAPPER); + verifyFailureMissingTypeId("{}", TrueCommand.class, ENABLED_MAPPER); + verifyFailureMissingTypeId("{}", TrueCommand.class, DEFAULT_MAPPER); + verifyFailureMissingTypeId("{}", TrueCommand.class, DISABLED_MAPPER); + verifyFailureMissingTypeId("{}", DefaultCommand.class, ENABLED_MAPPER); + verifyFailureMissingTypeId("{}", DefaultCommand.class, DEFAULT_MAPPER); + verifyFailureMissingTypeId("{}", DefaultCommand.class, DISABLED_MAPPER); + + // overrides : to require type id + verifySuccessWithNonNullAndType("{}", DoFalseCommand.class, ENABLED_MAPPER); + verifySuccessWithNonNullAndType("{}", DoFalseCommand.class, DEFAULT_MAPPER); + verifySuccessWithNonNullAndType("{}", DoFalseCommand.class, DISABLED_MAPPER); + // overrides : do not require type id + verifyFailureMissingTypeId("{}", DoTrueCommand.class, ENABLED_MAPPER); + verifyFailureMissingTypeId("{}", DoTrueCommand.class, DEFAULT_MAPPER); + verifyFailureMissingTypeId("{}", DoTrueCommand.class, DISABLED_MAPPER); + // overrides : defaults + verifyFailureMissingTypeId("{}", DoDefaultCommand.class, ENABLED_MAPPER); + verifyFailureMissingTypeId("{}", DoDefaultCommand.class, DEFAULT_MAPPER); + verifySuccessWithNonNullAndType("{}", DoDefaultCommand.class, DISABLED_MAPPER); + } + + public void testSuccessWhenTypeIdIsProvided() throws Exception { + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-false'}"), FalseCommand.class, ENABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-false'}"), FalseCommand.class, DEFAULT_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-false'}"), FalseCommand.class, DISABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-false'}"), DoFalseCommand.class, ENABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-false'}"), DoFalseCommand.class, DEFAULT_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-false'}"), DoFalseCommand.class, DISABLED_MAPPER); + + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-true'}"), TrueCommand.class, ENABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-true'}"), TrueCommand.class, DEFAULT_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-true'}"), TrueCommand.class, DISABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-true'}"), DoTrueCommand.class, ENABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-true'}"), DoTrueCommand.class, DEFAULT_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-true'}"), DoTrueCommand.class, DISABLED_MAPPER); + + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-default'}"), DefaultCommand.class, ENABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-default'}"), DefaultCommand.class, DEFAULT_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-default'}"), DefaultCommand.class, DISABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-default'}"), DoDefaultCommand.class, ENABLED_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-default'}"), DoDefaultCommand.class, DEFAULT_MAPPER); + verifySuccessWithNonNullAndType(a2q("{'@type': 'do-default'}"), DoDefaultCommand.class, DISABLED_MAPPER); + } + + private void verifySuccessWithNonNullAndType(String json, Class clazz, ObjectMapper om) throws Exception { + T bean = om.readValue(json, clazz); + assertNotNull(bean); + assertType(bean, clazz); + } + + private void verifyFailureMissingTypeId(String json, Class clazz, ObjectMapper om) throws Exception { + try { + om.readValue(json, clazz); + fail("Should not pass"); + } catch (InvalidTypeIdException e) { + verifyException(e, "missing type id property '@type'"); + } + } +}