DeserializationProblemHandler.handleUnexpectedToken() no longer invoked for array-like types (original) (raw)

Describe the bug
Before 2.12.x DeserializationProblemHandler::handleUnexpectedToken is invoked when trying to deserialize something with a structurally incompatible type, like deserializing a string from a START_OBJECT. In 2.12.x this no longer happens if the targeted type is an array-like type, such as an Iterable or a Collection. Instead the following exception is thrown: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `java.util.ArrayList` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('prop'), it appears that DeserializationProblemHandler::handleInstantiationProblem is invoked instead.

Version information
2.12.x
2.13.x

To Reproduce

pom.xml

4.0.0 org.example jackson-mve 1.0-SNAPSHOT jar UTF-8 1.8 1.8
    <!-- This and 2.13.0 exhibit the issue -->
    <jackson.version>2.12.0</jackson.version>
    <!-- This is the last version where the test passes -->
</properties>

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>${jackson.version}</version>
    </dependency>

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.5.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M3</version>
        </plugin>
    </plugins>
</build>

Test sample

package org.example.mve;

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

import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Objects;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.node.ObjectNode;

class ExceptionMappingProblemHandlerTest {

private static final String PROP = "prop";

private static final ObjectMapper MAPPER;

static {
    MAPPER = new ObjectMapper();
    MAPPER.addHandler(new ExceptionMappingProblemHandler());
}

@Test
void testHandleUnexpectedToken() {
    ObjectNode input = MAPPER.createObjectNode();
    input.set(PROP, MAPPER.createArrayNode());

    assertThrows(
        JsonStructuralMismatch.class,
        () -> MAPPER.treeToValue(input, StringProperty.class)
    );
}

@Test
void testHandleUnexpectedTokenArray() {
    ObjectNode input = MAPPER.createObjectNode();
    input.put(PROP, "prop");

    assertThrows(
        JsonStructuralMismatch.class,
        () -> MAPPER.treeToValue(input, Array.class)
    );
}

static class Array {

    private final Collection<String> prop;

    private Array(Collection<String> prop) {
        this.prop = prop;
    }

    @JsonCreator
    static Array create(@JsonProperty(PROP) Iterable<String> prop) {
        ArrayList<String> list = new ArrayList<>();
        prop.forEach(list::add);
        return new Array(list);
    }

    @JsonProperty(PROP)
    public Iterable<String> getProp() {
        return prop;
    }
}

static class StringProperty {

    private final String prop;

    private StringProperty(String prop) {
        this.prop = Objects.requireNonNull(prop, "prop must not be null");
    }

    @JsonCreator
    static StringProperty create(@JsonProperty(PROP) String prop) {
        return new StringProperty(prop);
    }

    @JsonProperty(PROP)
    public String getProp() {
        return prop;
    }
}

public static final class ExceptionMappingProblemHandler extends DeserializationProblemHandler {

    @Override
    public Object handleUnexpectedToken(
        DeserializationContext ctx,
        Class<?> targetType,
        JsonToken token,
        JsonParser parser,
        String failureMsg
    ) throws IOException {
        throw new JsonStructuralMismatch(parser, "Some text here");
    }

    @Override
    public Object handleUnexpectedToken(
        DeserializationContext ctxt,
        JavaType targetType,
        JsonToken t,
        JsonParser parser,
        String failureMsg
    ) throws IOException {
        throw new JsonStructuralMismatch(parser, "Some text here");
    }

}

public static class JsonStructuralMismatch extends MismatchedInputException {

    JsonStructuralMismatch(JsonParser parser, String description) {
        super(parser, description);
    }
}

}

Expected behavior
In the above test case, a JsonStructuralMismatch should be thrown for the array case as well.