StrictJsonTypeInfoHandling3853Test.java

package tools.jackson.databind.jsontype;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.OptBoolean;

import tools.jackson.databind.MapperFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.exc.InvalidTypeIdException;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.*;

public class StrictJsonTypeInfoHandling3853Test extends DatabindTestUtil {

    @JsonTypeInfo(use = Id.NAME)
    interface Command {
    }

    @JsonTypeName("do-something")
    static class DoSomethingCommand implements Command {
    }

    // [databind#3877]
    @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
    /**********************************************************
     */

    @Test
    public void testDefaultHasStrictTypeHandling() throws Exception {
        ObjectMapper om = jsonMapperBuilder()
                .registerSubtypes(DoSomethingCommand.class)
                .build();

        // 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 disallow the deserialization without a type if the target
        // is a concrete sub-type
        verifyInvalidTypeIdWithConcreteTarget(om);
    }

    @Test
    public void testExplicitNonStrictTypeHandling() throws Exception {
        ObjectMapper om = jsonMapperBuilder()
                .disable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES)
                .registerSubtypes(DoSomethingCommand.class)
                .build();

        // 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);
    }

    @Test
    public void testStrictTypeHandling() throws Exception {
        ObjectMapper om = jsonMapperBuilder()
                .enable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES)
                .registerSubtypes(DoSomethingCommand.class)
                .build();

        // 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);
    }

    // [databind#3877]
    @Test
    public void testMissingTypeId() throws Exception {
        final ObjectMapper ENABLED_MAPPER = jsonMapperBuilder().enable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES).build();
        final ObjectMapper DISABLED_MAPPER = jsonMapperBuilder().disable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES).build();
        final ObjectMapper DEFAULT_MAPPER = jsonMapperBuilder().build();

        // 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);
    }

    // [databind#3877]
    @Test
    public void testSuccessWhenTypeIdIsProvided() throws Exception {
        final ObjectMapper ENABLED_MAPPER = jsonMapperBuilder().enable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES).build();
        final ObjectMapper DISABLED_MAPPER = jsonMapperBuilder().disable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES).build();
        final ObjectMapper DEFAULT_MAPPER = jsonMapperBuilder().build();

        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);
    }

    /*
    /**********************************************************
    /* Helper methods
    /**********************************************************
     */

    private void verifyInvalidTypeIdWithSuperclassTarget(ObjectMapper om) throws Exception {
        InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
                () -> om.readValue("{}", Command.class));
        verifyException(e, "missing type id property '@type'");
    }

    private void verifyInvalidTypeIdWithConcreteTarget(ObjectMapper om) throws Exception {
        InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
                () -> om.readValue("{}", DoSomethingCommand.class));
        verifyException(e, "missing type id property '@type'");
    }

    private void verifyDeserializationWithConcreteTarget(ObjectMapper om) throws Exception {
        DoSomethingCommand cmd = om.readValue("{}", DoSomethingCommand.class);
        assertInstanceOf(DoSomethingCommand.class, cmd);
    }

    private void verifyDeserializationWithFullTypeInfo(ObjectMapper om) throws Exception {
        Command cmd = om.readValue("{\"@type\":\"do-something\"}", Command.class);
        assertInstanceOf(DoSomethingCommand.class, cmd);
        cmd = om.readValue("{\"@type\":\"do-something\"}", DoSomethingCommand.class);
        assertInstanceOf(DoSomethingCommand.class, cmd);
    }

    private <T> void verifySuccessWithNonNullAndType(String json, Class<T> clazz, ObjectMapper om) throws Exception {
        T bean = om.readValue(json, clazz);
        assertNotNull(bean);
        assertInstanceOf(clazz, bean);
    }

    private void verifyFailureMissingTypeId(String json, Class<?> clazz, ObjectMapper om) throws Exception {
        InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
                () -> om.readValue(json, clazz));
        verifyException(e, "missing type id property '@type'");
    }
}