OptionalJava8Fallbacks4082Test.java

package com.fasterxml.jackson.databind.interop;

import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
import com.fasterxml.jackson.databind.util.TokenBuffer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

// [databind#4082]: add fallback handling for Java 8 Optional types, to
// prevent accidental serialization as POJOs, as well as give more information
// on deserialization attempts
// [databind#5006]: add `MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS`
//
// @since 2.16 (changed in 2.19)
public class OptionalJava8Fallbacks4082Test extends DatabindTestUtil
{
    private final ObjectMapper MAPPER = newJsonMapper();

    ObjectMapper LENIENT_MAPPER = JsonMapper.builder()
            .disable(MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS)
            .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)
            .build();
    
    // Test to prevent serialization as POJO, without Java 8 date/time module:
    @Test
    public void testPreventSerialization() throws Exception {
        _testPreventSerialization(Optional.empty());
        _testPreventSerialization(OptionalInt.of(13));
        _testPreventSerialization(OptionalLong.of(-1L));
        _testPreventSerialization(OptionalDouble.of(0.5));
    }

    private void _testPreventSerialization(Object value) throws Exception
    {
        try {
            String json = MAPPER.writeValueAsString(value);
            fail("Should not pass, wrote out as\n: "+json);
        } catch (InvalidDefinitionException e) {
            verifyException(e, "Java 8 optional type `"+value.getClass().getName()
                    +"` not supported by default");
            verifyException(e, "add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jdk8\"");
            verifyException(e, "(or disable `MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS`");
        }
    }

    @Test
    public void testBetterDeserializationError() throws Exception
    {
        _testBetterDeserializationError(Optional.class);
        _testBetterDeserializationError(OptionalInt.class);
        _testBetterDeserializationError(OptionalLong.class);
        _testBetterDeserializationError(OptionalDouble.class);
    }

    private void _testBetterDeserializationError(Class<?> target) throws Exception
    {
        try {
            Object result = MAPPER.readValue(" 0 ", target);
            fail("Not expecting to pass, resulted in: "+result);
        } catch (InvalidDefinitionException e) {
            verifyException(e, "Java 8 optional type `"+target.getName()+"` not supported by default");
            verifyException(e, "add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jdk8\"");
            verifyException(e, "(or disable `MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS`");
        }
    }

    // But, [databind#3091], allow deser from JsonToken.VALUE_EMBEDDED_OBJECT
    @Test
    public void testAllowAsEmbedded() throws Exception
    {
        Optional<Object> optValue = Optional.empty();
        try (TokenBuffer tb = new TokenBuffer((ObjectCodec) null, false)) {
            tb.writeEmbeddedObject(optValue);

            try (JsonParser p = tb.asParser()) {
                Optional<?>  result = MAPPER.readValue(p, Optional.class);
                assertSame(optValue, result);
            }
        }

        // but also try deser into an array
        try (TokenBuffer tb = new TokenBuffer((ObjectCodec) null, false)) {
            tb.writeStartArray();
            tb.writeEmbeddedObject(optValue);
            tb.writeEndArray();

            try (JsonParser p = tb.asParser()) {
                Object[] result = MAPPER.readValue(p, Object[].class);
                assertNotNull(result);
                assertEquals(1, result.length);
                assertSame(optValue, result[0]);
            }
        }
    }

    // [databind#5006]
    @Test
    public void testAllowDeserializationWithFeature() throws Exception
    {
        _testAllowDeserializationLenient(Optional.class);
        _testAllowDeserializationLenient(OptionalInt.class);
        _testAllowDeserializationLenient(OptionalLong.class);
        _testAllowDeserializationLenient(OptionalDouble.class);
    }

    private void _testAllowDeserializationLenient(Class<?> target) throws Exception {
        JacksonException e = assertThrows(JacksonException.class, () ->
                LENIENT_MAPPER.readValue("{}", target));
        assertThat(e).hasMessageContaining("Cannot construct instance of `"+target.getName()+"`");
    }

    // [databind#5006]
    @Test
    public void testAllowSerializationWithFeature() throws Exception
    {
        assertThat(LENIENT_MAPPER.writeValueAsString(Optional.empty())).contains("\"present\":false");
        assertThat(LENIENT_MAPPER.writeValueAsString(OptionalInt.of(13))).contains("\"asInt\":13");
        assertThat(LENIENT_MAPPER.writeValueAsString(OptionalLong.of(-1L))).contains("\"asLong\":-1");
        assertThat(LENIENT_MAPPER.writeValueAsString(OptionalDouble.of(0.5))).contains("\"asDouble\":0.5");
    }
}