CoerceToBooleanTest.java

package com.fasterxml.jackson.databind.convert;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.concurrent.atomic.AtomicBoolean;

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.core.StreamReadFeature;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.cfg.CoercionAction;
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.type.LogicalType;

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

import static com.fasterxml.jackson.databind.testutil.DatabindTestUtil.*;

public class CoerceToBooleanTest
{
    static class BooleanPOJO {
        public boolean value;

        public void setValue(boolean v) { value = v; }
    }

    static class BooleanPrimitiveBean
    {
        public boolean booleanValue = true;

        public void setBooleanValue(boolean v) { booleanValue = v; }
    }

    static class BooleanWrapper {
        public Boolean wrapper;
        public boolean primitive;

        protected Boolean ctor;

        @JsonCreator
        public BooleanWrapper(@JsonProperty("ctor") Boolean foo) {
            ctor = foo;
        }

        public void setWrapper(Boolean v) { wrapper = v; }
        public void setPrimitive(boolean v) { primitive = v; }
    }

    private final ObjectMapper DEFAULT_MAPPER = newJsonMapper();

    private final ObjectMapper LEGACY_NONCOERCING_MAPPER = jsonMapperBuilder()
            .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
            // 30-May-2025, tatu: Needed after [core#1438] (clear current token on close)
            .disable(StreamReadFeature.CLEAR_CURRENT_TOKEN_ON_CLOSE)
            .build();

    private final ObjectMapper MAPPER_INT_TO_EMPTY = jsonMapperBuilder()
            .withCoercionConfig(LogicalType.Boolean, cfg ->
                cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.AsEmpty))
            .build();

    private final ObjectMapper MAPPER_INT_TRY_CONVERT = jsonMapperBuilder()
            .withCoercionConfig(LogicalType.Boolean, cfg ->
                cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.TryConvert))
            .build();

    private final ObjectMapper MAPPER_INT_TO_NULL = jsonMapperBuilder()
            .withCoercionConfig(LogicalType.Boolean, cfg ->
                cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.AsNull))
            .build();

    private final ObjectMapper MAPPER_TO_FAIL = jsonMapperBuilder()
            .withCoercionConfig(LogicalType.Boolean, cfg ->
                cfg.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail))
            .build();

    private final static String DOC_WITH_0 = a2q("{'value':0}");
    private final static String DOC_WITH_1 = a2q("{'value':1}");

    /*
    /**********************************************************
    /* Unit tests: default, legacy configuration, from String
    /**********************************************************
     */

    // for [databind#403]
    @Test
    public void testEmptyStringFailForBooleanPrimitive() throws IOException
    {
        final ObjectReader reader = DEFAULT_MAPPER
                .readerFor(BooleanPrimitiveBean.class)
                .with(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);
        try {
            reader.readValue(a2q("{'booleanValue':''}"));
            fail("Expected failure for boolean + empty String");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot coerce `null` to `boolean`");
            verifyException(e, "FAIL_ON_NULL_FOR_PRIMITIVES");
        }
    }

    @Test
    public void testStringToBooleanCoercionOk() throws Exception
    {
        // first successful coercions. Boolean has a ton...
        _verifyCoerceSuccess(DEFAULT_MAPPER, "1", Boolean.TYPE, Boolean.TRUE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, "1", Boolean.class, Boolean.TRUE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("true"), Boolean.TYPE, Boolean.TRUE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("true"), Boolean.class, Boolean.TRUE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("True"), Boolean.TYPE, Boolean.TRUE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("True"), Boolean.class, Boolean.TRUE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("TRUE"), Boolean.TYPE, Boolean.TRUE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("TRUE"), Boolean.class, Boolean.TRUE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, "0", Boolean.TYPE, Boolean.FALSE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, "0", Boolean.class, Boolean.FALSE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("false"), Boolean.TYPE, Boolean.FALSE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("false"), Boolean.class, Boolean.FALSE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("False"), Boolean.TYPE, Boolean.FALSE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("False"), Boolean.class, Boolean.FALSE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("FALSE"), Boolean.TYPE, Boolean.FALSE);
        _verifyCoerceSuccess(DEFAULT_MAPPER, q("FALSE"), Boolean.class, Boolean.FALSE);
    }

    private void _verifyCoerceSuccess(ObjectMapper mapper,
            String input, Class<?> type, Object exp) throws IOException
    {
        Object result = mapper.readerFor(type)
                .readValue(input);
        assertEquals(exp, result);
    }

    @Test
    public void testStringToBooleanCoercionFail() throws Exception
    {
        _verifyRootStringCoerceFail(LEGACY_NONCOERCING_MAPPER, "true", Boolean.TYPE);
        _verifyRootStringCoerceFail(LEGACY_NONCOERCING_MAPPER, "true", Boolean.class);
        _verifyRootStringCoerceFail(LEGACY_NONCOERCING_MAPPER, "True", Boolean.TYPE);
        _verifyRootStringCoerceFail(LEGACY_NONCOERCING_MAPPER, "True", Boolean.class);
        _verifyRootStringCoerceFail(LEGACY_NONCOERCING_MAPPER, "TRUE", Boolean.TYPE);
        _verifyRootStringCoerceFail(LEGACY_NONCOERCING_MAPPER, "TRUE", Boolean.class);

        _verifyRootStringCoerceFail(LEGACY_NONCOERCING_MAPPER, "false", Boolean.TYPE);
        _verifyRootStringCoerceFail(LEGACY_NONCOERCING_MAPPER, "false", Boolean.class);
    }

    private void _verifyRootStringCoerceFail(ObjectMapper nonCoercingMapper,
            String unquotedValue, Class<?> type) throws IOException
    {
        // Test failure for root value: for both byte- and char-backed sources:

        final String input = q(unquotedValue);
        try (JsonParser p = nonCoercingMapper.createParser(new StringReader(input))) {
            _verifyStringCoerceFail(nonCoercingMapper, p, unquotedValue, type);
        }
        final byte[] inputBytes = utf8Bytes(input);
        try (JsonParser p = nonCoercingMapper.createParser(new ByteArrayInputStream(inputBytes))) {
            _verifyStringCoerceFail(nonCoercingMapper, p, unquotedValue, type);
        }
    }

    private void _verifyStringCoerceFail(ObjectMapper nonCoercingMapper,
            JsonParser p,
            String unquotedValue, Class<?> type) throws IOException
    {
        try {
            nonCoercingMapper.readerFor(type)
                .readValue(p);
            fail("Should not have allowed coercion");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot coerce ");
            verifyException(e, " to `");
            verifyException(e, "` value");

            assertSame(p, e.getProcessor());

            assertToken(JsonToken.VALUE_STRING, p.currentToken());
            assertEquals(unquotedValue, p.getText());
        }
    }

    /*
    /**********************************************************
    /* Unit tests: default, legacy configuration, from Int
    /**********************************************************
     */

    @Test
    public void testIntToBooleanCoercionSuccessPojo() throws Exception
    {
        BooleanPOJO p;
        final ObjectReader r = DEFAULT_MAPPER.readerFor(BooleanPOJO.class);

        p = r.readValue(DOC_WITH_0);
        assertFalse(p.value);
        p = r.readValue(utf8Bytes(DOC_WITH_0));
        assertFalse(p.value);

        p = r.readValue(DOC_WITH_1);
        assertTrue(p.value);
        p = r.readValue(utf8Bytes(DOC_WITH_1));
        assertTrue(p.value);
    }

    @Test
    public void testIntToBooleanCoercionSuccessRoot() throws Exception
    {
        final ObjectReader br = DEFAULT_MAPPER.readerFor(Boolean.class);

        assertEquals(Boolean.FALSE, br.readValue(" 0"));
        assertEquals(Boolean.FALSE, br.readValue(utf8Bytes(" 0")));
        assertEquals(Boolean.TRUE, br.readValue(" -1"));
        assertEquals(Boolean.TRUE, br.readValue(utf8Bytes(" -1")));

        final ObjectReader atomicR = DEFAULT_MAPPER.readerFor(AtomicBoolean.class);

        AtomicBoolean ab;

        ab = atomicR.readValue(" 0");
        ab = atomicR.readValue(utf8Bytes(" 0"));
        assertFalse(ab.get());

        ab = atomicR.readValue(" 111");
        assertTrue(ab.get());
        ab = atomicR.readValue(utf8Bytes(" 111"));
        assertTrue(ab.get());
    }

    // Test for verifying that Long values are coerced to boolean correctly as well
    @Test
    public void testLongToBooleanCoercionOk() throws Exception
    {
        long value = 1L + Integer.MAX_VALUE;
        BooleanWrapper b = DEFAULT_MAPPER.readValue("{\"primitive\" : "+value+", \"wrapper\":"+value+", \"ctor\":"+value+"}",
                BooleanWrapper.class);
        assertEquals(Boolean.TRUE, b.wrapper);
        assertTrue(b.primitive);
        assertEquals(Boolean.TRUE, b.ctor);

        // but ensure we can also get `false`
        b = DEFAULT_MAPPER.readValue("{\"primitive\" : 0 , \"wrapper\":0, \"ctor\":0}",
                BooleanWrapper.class);
        assertEquals(Boolean.FALSE, b.wrapper);
        assertFalse(b.primitive);
        assertEquals(Boolean.FALSE, b.ctor);

        boolean[] boo = DEFAULT_MAPPER.readValue("[ 0, 15, \"\", \"false\", \"True\" ]",
                boolean[].class);
        assertEquals(5, boo.length);
        assertFalse(boo[0]);
        assertTrue(boo[1]);
        assertFalse(boo[2]);
        assertFalse(boo[3]);
        assertTrue(boo[4]);
    }

    // [databind#2635], [databind#2770]
    @Test
    public void testIntToBooleanCoercionFailBytes() throws Exception
    {
        _verifyBooleanCoerceFail(a2q("{'value':1}"), true, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class);

        _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE);
        _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class);

        _verifyBooleanCoerceFail("1.25", true, JsonToken.VALUE_NUMBER_FLOAT, "1.25", Boolean.TYPE);
        _verifyBooleanCoerceFail("1.25", true, JsonToken.VALUE_NUMBER_FLOAT, "1.25", Boolean.class);
    }

    // [databind#2635], [databind#2770]
    @Test
    public void testIntToBooleanCoercionFailChars() throws Exception
    {
        _verifyBooleanCoerceFail(a2q("{'value':1}"), false, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class);

        _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE);
        _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class);

        _verifyBooleanCoerceFail("1.25", false, JsonToken.VALUE_NUMBER_FLOAT, "1.25", Boolean.TYPE);
        _verifyBooleanCoerceFail("1.25", false, JsonToken.VALUE_NUMBER_FLOAT, "1.25", Boolean.class);
    }

    /*
    /**********************************************************
    /* Unit tests: new CoercionConfig, as-null, as-empty, try-coerce
    /**********************************************************
     */

    @Test
    public void testIntToNullCoercion() throws Exception
    {
        assertNull(MAPPER_INT_TO_NULL.readValue("0", Boolean.class));
        assertNull(MAPPER_INT_TO_NULL.readValue("1", Boolean.class));

        // but due to coercion to `boolean`, can not return null here -- however,
        // goes "1 -> false (no null for primitive) -> Boolean.FALSE
        assertEquals(Boolean.FALSE, MAPPER_INT_TO_NULL.readValue("0", Boolean.TYPE));
        assertEquals(Boolean.FALSE, MAPPER_INT_TO_NULL.readValue("1", Boolean.TYPE));

        // As to AtomicBoolean: that type itself IS nullable since it's of LogicalType.Boolean so
        assertNull(MAPPER_INT_TO_NULL.readValue("0", AtomicBoolean.class));
        assertNull(MAPPER_INT_TO_NULL.readValue("1", AtomicBoolean.class));

        BooleanPOJO p;
        p = MAPPER_INT_TO_NULL.readValue(DOC_WITH_0, BooleanPOJO.class);
        assertFalse(p.value);
        p = MAPPER_INT_TO_NULL.readValue(DOC_WITH_1, BooleanPOJO.class);
        assertFalse(p.value);
    }

    @Test
    public void testIntToEmptyCoercion() throws Exception
    {
        // "empty" value for Boolean/boolean is False/false

        assertEquals(Boolean.FALSE, MAPPER_INT_TO_EMPTY.readValue("0", Boolean.class));
        assertEquals(Boolean.FALSE, MAPPER_INT_TO_EMPTY.readValue("1", Boolean.class));

        assertEquals(Boolean.FALSE, MAPPER_INT_TO_EMPTY.readValue("0", Boolean.TYPE));
        assertEquals(Boolean.FALSE, MAPPER_INT_TO_EMPTY.readValue("1", Boolean.TYPE));

        AtomicBoolean ab;
        ab = MAPPER_INT_TO_EMPTY.readValue("0", AtomicBoolean.class);
        assertFalse(ab.get());
        ab = MAPPER_INT_TO_EMPTY.readValue("1", AtomicBoolean.class);
        assertFalse(ab.get());

        BooleanPOJO p;
        p = MAPPER_INT_TO_EMPTY.readValue(DOC_WITH_0, BooleanPOJO.class);
        assertFalse(p.value);
        p = MAPPER_INT_TO_EMPTY.readValue(DOC_WITH_1, BooleanPOJO.class);
        assertFalse(p.value);
    }

    @Test
    public void testIntToTryCoercion() throws Exception
    {
        // And "TryCoerce" should do what would be typically expected

        assertEquals(Boolean.FALSE, MAPPER_INT_TRY_CONVERT.readValue("0", Boolean.class));
        assertEquals(Boolean.TRUE, MAPPER_INT_TRY_CONVERT.readValue("1", Boolean.class));

        assertEquals(Boolean.FALSE, MAPPER_INT_TRY_CONVERT.readValue("0", Boolean.TYPE));
        assertEquals(Boolean.TRUE, MAPPER_INT_TRY_CONVERT.readValue("1", Boolean.TYPE));

        AtomicBoolean ab;
        ab = MAPPER_INT_TRY_CONVERT.readValue("0", AtomicBoolean.class);
        assertFalse(ab.get());
        ab = MAPPER_INT_TRY_CONVERT.readValue("1", AtomicBoolean.class);
        assertTrue(ab.get());

        BooleanPOJO p;
        p = MAPPER_INT_TRY_CONVERT.readValue(DOC_WITH_0, BooleanPOJO.class);
        assertFalse(p.value);
        p = MAPPER_INT_TRY_CONVERT.readValue(DOC_WITH_1, BooleanPOJO.class);
        assertTrue(p.value);
    }

    /*
    /**********************************************************
    /* Unit tests: new CoercionConfig, failing
    /**********************************************************
     */

    @Test
    public void testFailFromInteger() throws Exception
    {
        _verifyFailFromInteger(MAPPER_TO_FAIL, BooleanPOJO.class, DOC_WITH_0, Boolean.TYPE);
        _verifyFailFromInteger(MAPPER_TO_FAIL, BooleanPOJO.class, DOC_WITH_1, Boolean.TYPE);

        _verifyFailFromInteger(MAPPER_TO_FAIL, Boolean.class, "0");
        _verifyFailFromInteger(MAPPER_TO_FAIL, Boolean.class, "42");

        _verifyFailFromInteger(MAPPER_TO_FAIL, Boolean.TYPE, "0");
        _verifyFailFromInteger(MAPPER_TO_FAIL, Boolean.TYPE, "999");

        _verifyFailFromInteger(MAPPER_TO_FAIL, AtomicBoolean.class, "0");
        _verifyFailFromInteger(MAPPER_TO_FAIL, AtomicBoolean.class, "-123");
    }

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

    private void _verifyBooleanCoerceFail(String doc, boolean useBytes,
            JsonToken tokenType, String tokenValue, Class<?> targetType) throws IOException
    {
        // Test failure for root value: for both byte- and char-backed sources.

        // [databind#2635]: important, need to use `readValue()` that takes content and NOT
        // JsonParser, as this forces closing of underlying parser and exposes more issues.

        final ObjectReader r = LEGACY_NONCOERCING_MAPPER.readerFor(targetType);
        try {
            if (useBytes) {
                r.readValue(utf8Bytes(doc));
            } else {
                r.readValue(doc);
            }
            fail("Should not have allowed coercion");
        } catch (MismatchedInputException e) {
            _verifyBooleanCoerceFailReason(e, tokenType, tokenValue);
        }
    }

    @SuppressWarnings("resource")
    private void _verifyBooleanCoerceFailReason(MismatchedInputException e,
            JsonToken tokenType, String tokenValue) throws IOException
    {
        verifyException(e, "Cannot coerce ", "Cannot deserialize value of type ");
        // 30-May-2025, tatu: [databind#5179] got access via exception now
        assertToken(tokenType, e.getCurrentToken());

        JsonParser p = (JsonParser) e.getProcessor();
        final String text = p.getText();
        if (!tokenValue.equals(text)) {
            String textDesc = (text == null) ? "NULL" : q(text);
            fail("Token text ("+textDesc+") via parser of type "+p.getClass().getName()
                    +" not as expected ("+q(tokenValue)+")");
        }
    }

    private void _verifyFailFromInteger(ObjectMapper m, Class<?> targetType, String doc) throws Exception {
        _verifyFailFromInteger(m, targetType, doc, targetType);
    }

    private void _verifyFailFromInteger(ObjectMapper m, Class<?> targetType, String doc,
            Class<?> valueType) throws Exception
    {
        try {
            m.readerFor(targetType).readValue(doc);
            fail("Should not accept Integer for "+targetType.getName()+" by default");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot coerce Integer value");
            verifyException(e, "to `"+valueType.getName()+"`");
        }
    }
}