Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new @JsonTypeInfo.requireTypeIdForSubtypes usage #3891

Merged
merged 12 commits into from
Jun 6, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> void verifySuccessWithNonNullAndType(String json, Class<T> 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'");
}
}
}