NullStringValueDeserTest.java

package tools.jackson.databind.deser;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.*;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.util.JsonParserDelegate;

import tools.jackson.databind.*;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

/**
 * Tests to verify that a parser reporting {@code VALUE_STRING} but returning
 * {@code null} as its text (like Ion does for corrupt content, e.g. a SYMBOL
 * whose text cannot be resolved) results in a {@link MismatchedInputException}
 * and not a {@link NullPointerException}.
 */
public class NullStringValueDeserTest extends DatabindTestUtil
{
    enum ABC { A, B, C }

    static class IntBean {
        public int x;
    }

    static class DateBean {
        @JsonFormat(pattern = "yyyy-MM-dd")
        public Date date;
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_ARRAY)
    @JsonSubTypes(@JsonSubTypes.Type(value = Impl.class, name = "impl"))
    static class Base { }

    static class Impl extends Base { }

    // Mimics Ion parser behavior for corrupt content (e.g. a SYMBOL whose text
    // cannot be resolved): reports VALUE_STRING but returns null text
    static class NullStringParser extends JsonParserDelegate
    {
        public NullStringParser(JsonParser p) {
            super(p);
        }

        @Override
        public String getString() {
            return hasToken(JsonToken.VALUE_STRING) ? null : delegate.getString();
        }

        @Override
        public String getValueAsString() {
            return hasToken(JsonToken.VALUE_STRING) ? null : delegate.getValueAsString();
        }

        @Override
        public String getValueAsString(String defaultValue) {
            return hasToken(JsonToken.VALUE_STRING) ? null : delegate.getValueAsString(defaultValue);
        }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    private final ObjectMapper WRAPPING_MAPPER = jsonMapperBuilder()
            .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
            .build();

    // Containers

    @Test
    public void nullStringAsMap() throws Exception {
        _verifyFails(MAPPER, Map.class);
    }

    @Test
    public void nullStringAsEnumMap() throws Exception {
        _verifyFails(MAPPER, MAPPER.getTypeFactory().constructMapType(EnumMap.class, ABC.class, String.class));
    }

    @Test
    public void nullStringAsList() throws Exception {
        _verifyFails(MAPPER, List.class);
    }

    @Test
    public void nullStringAsIntegerList() throws Exception {
        _verifyFails(MAPPER, MAPPER.getTypeFactory().constructCollectionType(List.class, Integer.class));
    }

    // Scalars

    @Test
    public void nullStringAsNumbers() throws Exception {
        for (Class<?> type : new Class<?>[] {
                Boolean.TYPE, Boolean.class,
                Byte.TYPE, Byte.class, Short.TYPE, Short.class,
                Integer.TYPE, Integer.class, Long.TYPE, Long.class,
                Float.TYPE, Float.class, Double.TYPE, Double.class,
                Number.class,
                BigInteger.class, BigDecimal.class
        }) {
            _verifyFails(MAPPER, type);
        }
    }

    // `char` / `Character` with null text is handled like a JSON null
    @Test
    public void nullStringAsCharacter() throws Exception {
        ObjectMapper lenient = jsonMapperBuilder()
                .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
                .build();
        assertEquals(Character.valueOf('\0'), _read(lenient, lenient.constructType(Character.TYPE), "'abc'"));
        assertNull(_read(lenient, lenient.constructType(Character.class), "'abc'"));
        ObjectMapper strict = jsonMapperBuilder()
                .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
                .build();
        _verifyFails(strict, Character.TYPE);
        assertNull(_read(strict, strict.constructType(Character.class), "'abc'"));
    }

    @Test
    public void nullStringAsIntProperty() throws Exception {
        _verifyFails(MAPPER, MAPPER.constructType(IntBean.class), "{'x':'abc'}");
    }

    @Test
    public void nullStringAsDate() throws Exception {
        _verifyFails(MAPPER, Date.class);
        _verifyFails(MAPPER, MAPPER.constructType(DateBean.class), "{'date':'abc'}");
    }

    @Test
    public void nullStringAsEnum() throws Exception {
        _verifyFails(MAPPER, ABC.class);
        _verifyFails(MAPPER, MAPPER.getTypeFactory().constructCollectionType(EnumSet.class, ABC.class),
                "['abc']");
    }

    @Test
    public void nullStringAsPath() throws Exception {
        _verifyFails(MAPPER, Path.class);
    }

    // Arrays

    // `char[]` element with null text is handled like a JSON null element
    @Test
    public void nullStringAsCharArrayElement() throws Exception {
        ObjectMapper lenient = jsonMapperBuilder()
                .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
                .build();
        assertArrayEquals(new char[] { '\0', '\0' },
                (char[]) _read(lenient, lenient.constructType(char[].class), "['a','b']"));
        ObjectMapper strict = jsonMapperBuilder()
                .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
                .build();
        _verifyFails(strict, strict.constructType(char[].class), "['a','b']");
    }

    @Test
    public void nullStringAsSingleValueArray() throws Exception {
        _verifyFails(WRAPPING_MAPPER, int[].class);
        _verifyFails(WRAPPING_MAPPER, Integer[].class);
    }

    @Test
    public void nullStringAsSingleValueNullableContainers() throws Exception {
        // String (and untyped) values may be null, so these bind null element
        assertArrayEquals(new String[] { null },
                (String[]) _read(WRAPPING_MAPPER, WRAPPING_MAPPER.constructType(String[].class), "'abc'"));
        assertArrayEquals(new Object[] { null },
                (Object[]) _read(WRAPPING_MAPPER, WRAPPING_MAPPER.constructType(Object[].class), "'abc'"));
        assertEquals(Collections.singletonList(null),
                _read(WRAPPING_MAPPER, WRAPPING_MAPPER.getTypeFactory()
                        .constructCollectionType(List.class, String.class), "'abc'"));
    }

    // Polymorphic

    @Test
    public void nullStringAsWrapperArrayTypeId() throws Exception {
        _verifyFails(MAPPER, MAPPER.constructType(Base.class), "['impl',{}]");
    }

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

    private void _verifyFails(ObjectMapper mapper, Class<?> type) throws Exception {
        _verifyFails(mapper, mapper.constructType(type));
    }

    private void _verifyFails(ObjectMapper mapper, JavaType type) throws Exception {
        _verifyFails(mapper, type, "'abc'");
    }

    private void _verifyFails(ObjectMapper mapper, JavaType type, String json) throws Exception {
        try {
            _read(mapper, type, json);
            fail("Should not pass, got result for " + type);
        } catch (MismatchedInputException e) {
            // expected
        }
    }

    private Object _read(ObjectMapper mapper, JavaType type, String json) throws Exception {
        try (JsonParser p = new NullStringParser(mapper.createParser(a2q(json)))) {
            return mapper.readValue(p, type);
        }
    }
}