Skip to content

Commit

Permalink
Add new @JsonTypeInfo.requireTypeIdForSubtypes usage (#3891)
Browse files Browse the repository at this point in the history
Implements #3877
  • Loading branch information
JooHyukKim authored Jun 6, 2023
1 parent 58c2319 commit c5eabf8
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 1 deletion.
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'");
}
}
}

0 comments on commit c5eabf8

Please sign in to comment.