5

I am using Jackson 2.10.x to deserialise a Json of the format { myKey: "true"}. The possible variations are { myKey: "True"}, { myKey: "TRUE"} and similarly for false. The POJO that I need to deserialize to has attribute myKey::Boolean.class.

I do not own the POJO source and hence cannot set Json property on the specific attribute.

Jackson is able to deserialize when the value is "true" and "True" but not when it is "TRUE". I tried using the MapperFeature ACCEPT_CASE_INSENSITIVE_VALUES as follows but that did not help

objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES)

The exception message is

Cannot deserialize value of type `java.lang.Boolean` from String "TRUE": only "true" or "false" recognized at [Source: UNKNOWN; line: -1, column: -1] 

1 Answers1

6

You can add your custom com.fasterxml.jackson.databind.deser.DeserializationProblemHandler and implement handleWeirdStringValue method in which you can check text and return Boolean.TRUE or Boolean.FALSE for other cases you want to handle:

import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.json.JsonMapper;

import java.io.IOException;

public class JsonBooleanApp {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = JsonMapper.builder()
                .addHandler(new DeserializationProblemHandler() {
                    @Override
                    public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType, String valueToConvert, String failureMsg) throws IOException {
                        if (targetType == Boolean.class) {
                            return Boolean.TRUE.toString().equalsIgnoreCase(valueToConvert);
                        }
                        return super.handleWeirdStringValue(ctxt, targetType, valueToConvert, failureMsg);
                    }
                })
                .build();

        System.out.println(mapper.readValue("{\"value\": \"True\"}", BooleanHolder.class));
        System.out.println(mapper.readValue("{\"value\": \"true\"}", BooleanHolder.class));
        System.out.println(mapper.readValue("{\"value\": \"TRUE\"}", BooleanHolder.class));
    }
}

class BooleanHolder {
    private Boolean value;

    public Boolean getValue() {
        return value;
    }

    public void setValue(Boolean value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "BooleanHolder{" +
                "value=" + value +
                '}';
    }
}

Above code prints:

BooleanHolder{value=true}
BooleanHolder{value=true}
BooleanHolder{value=true}

Enable MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES

Default Boolean deserialiser in version 2.10.0 does not check MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES feature and is a final class which does not allow to override it easily. To make it aware about a feature we need to create a copy-paste version with some changes. To make it as close as possible to original I created com.fasterxml.jackson.databind.deser.std package and moved there below class:

package com.fasterxml.jackson.databind.deser.std;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;

import java.io.IOException;

public final class BooleanDeserializerIgnoreCase extends NumberDeserializers.PrimitiveOrWrapperDeserializer<Boolean> {
    private static final long serialVersionUID = 1L;

    public final static BooleanDeserializerIgnoreCase primitiveInstance = new BooleanDeserializerIgnoreCase(Boolean.TYPE, Boolean.FALSE);
    public final static BooleanDeserializerIgnoreCase wrapperInstance = new BooleanDeserializerIgnoreCase(Boolean.class, null);

    public BooleanDeserializerIgnoreCase(Class<Boolean> cls, Boolean nvl) {
        super(cls, nvl, Boolean.FALSE);
    }

    @Override
    public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonToken t = p.getCurrentToken();
        if (t == JsonToken.VALUE_TRUE) {
            return Boolean.TRUE;
        }
        if (t == JsonToken.VALUE_FALSE) {
            return Boolean.FALSE;
        }
        return _parseBoolean(p, ctxt);
    }

    // Since we can never have type info ("natural type"; String, Boolean, Integer, Double):
    // (is it an error to even call this version?)
    @Override
    public Boolean deserializeWithType(JsonParser p, DeserializationContext ctxt,
                                       TypeDeserializer typeDeserializer)
            throws IOException {
        JsonToken t = p.getCurrentToken();
        if (t == JsonToken.VALUE_TRUE) {
            return Boolean.TRUE;
        }
        if (t == JsonToken.VALUE_FALSE) {
            return Boolean.FALSE;
        }
        return _parseBoolean(p, ctxt);
    }

    protected final Boolean _parseBoolean(JsonParser p, DeserializationContext ctxt)
            throws IOException {
        JsonToken t = p.getCurrentToken();
        if (t == JsonToken.VALUE_NULL) {
            return (Boolean) _coerceNullToken(ctxt, _primitive);
        }
        if (t == JsonToken.START_ARRAY) { // unwrapping?
            return _deserializeFromArray(p, ctxt);
        }
        // should accept ints too, (0 == false, otherwise true)
        if (t == JsonToken.VALUE_NUMBER_INT) {
            return Boolean.valueOf(_parseBooleanFromInt(p, ctxt));
        }
        // And finally, let's allow Strings to be converted too
        if (t == JsonToken.VALUE_STRING) {
            return _deserializeFromString(p, ctxt);
        }
        // usually caller should have handled but:
        if (t == JsonToken.VALUE_TRUE) {
            return Boolean.TRUE;
        }
        if (t == JsonToken.VALUE_FALSE) {
            return Boolean.FALSE;
        }
        // Otherwise, no can do:
        return (Boolean) ctxt.handleUnexpectedToken(_valueClass, p);
    }

    protected final Boolean _deserializeFromString(JsonParser p, DeserializationContext ctxt) throws IOException {
        String text = p.getText().trim();

        if (ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES)) {
            if (Boolean.TRUE.toString().equalsIgnoreCase(text)) {
                return Boolean.TRUE;
            }
            if (Boolean.FALSE.toString().equalsIgnoreCase(text)) {
                return Boolean.FALSE;
            }
        } else {
            if ("true".equals(text) || "True".equals(text)) {
                _verifyStringForScalarCoercion(ctxt, text);
                return Boolean.TRUE;
            }
            if ("false".equals(text) || "False".equals(text)) {
                _verifyStringForScalarCoercion(ctxt, text);
                return Boolean.FALSE;
            }
            if (text.length() == 0) {
                return (Boolean) _coerceEmptyString(ctxt, _primitive);
            }
            if (_hasTextualNull(text)) {
                return (Boolean) _coerceTextualNull(ctxt, _primitive);
            }
        }
        return (Boolean) ctxt.handleWeirdStringValue(_valueClass, text,
                "only \"true\" or \"false\" recognized");
    }
}

Test case:

import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.BooleanDeserializerIgnoreCase;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

public class JsonBooleanApp {

    public static void main(String[] args) throws Exception {
        SimpleModule booleanIgnoreCaseModule = new SimpleModule();
        booleanIgnoreCaseModule.addDeserializer(Boolean.class, BooleanDeserializerIgnoreCase.wrapperInstance);
        booleanIgnoreCaseModule.addDeserializer(boolean.class, BooleanDeserializerIgnoreCase.primitiveInstance);

        ObjectMapper mapper = JsonMapper.builder()
                .addModule(booleanIgnoreCaseModule)
                .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES)
                .build();
        System.out.println(mapper.readValue("{\"value\": \"True\"}", BooleanHolder.class));
        System.out.println(mapper.readValue("{\"value\": \"true\"}", BooleanHolder.class));
        System.out.println(mapper.readValue("{\"value\": \"TRUE\"}", BooleanHolder.class));
    }
}

class BooleanHolder {
    private Boolean value;

    public Boolean getValue() {
        return value;
    }

    public void setValue(Boolean value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "BooleanHolder{" +
                "value=" + value +
                '}';
    }
}

Above code prints:

BooleanHolder{value=true}
BooleanHolder{value=true}
BooleanHolder{value=true}
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • Thanks a lot for getting back so quickly. I guess in absence of built-in support for Boolean deserialization using MapperFeature flags, custom deserializer is the best way to go. Any idea how the MapperFeature flags are actually utilised when deserailizing? If I see an example, maybe I could send a pull request to Jackson-databind to consider the Feature flag when deserializing json. – ghanekar.omkar Nov 22 '19 at 19:46
  • By default `com.fasterxml.jackson.databind.deser.std.NumberDeserializers.BooleanDeserializer` class is used to deserialise a `Boolean`. For you interesting would be a `_parseBoolean` method which implements conversion from `String` to `Boolean`. There were an issue [databind#422](https://github.com/FasterXML/jackson-databind/issues/422) which allows `"true"` and `"True"` values. I think if they wanted to use `ignore case` they could implement it already. When value is not handled then by default engine tries to check whether user not implemented `handleWeirdStringValue` method which we used. – Michał Ziober Nov 22 '19 at 19:57
  • @user956796 - if `BooleanDeserializer` would not delegate process to `handleWeirdStringValue` method I would suggest to implement custom deserialiser. Take a look on another example: [How to write boolean value as String in a json array?](https://stackoverflow.com/questions/16564639/how-to-write-boolean-value-as-string-in-a-json-array). – Michał Ziober Nov 22 '19 at 19:58
  • which is weird since the pull request you mentioned got approved but a [subsequent one](https://github.com/FasterXML/jackson-databind/pull/2132) requesting complete case insensitivity got declined citing availability of better control mechanisms like `ACCEPT_CASE_INSENSITIVE_VALUES `, which do not seem to work – ghanekar.omkar Nov 22 '19 at 21:18
  • @ghanekar.omkar, From documentation [ACCEPT_CASE_INSENSITIVE_VALUES](http://fasterxml.github.io/jackson-databind/javadoc/2.10/com/fasterxml/jackson/databind/MapperFeature.html#ACCEPT_CASE_INSENSITIVE_VALUES): "Feature that permits parsing some enumerated text-based value types but ignoring the case of the values on deserialization: for example, date/time type deserializers. Support for this feature depends on deserializer implementations using it." I checked a code and it does not use this feature. I can try provide some possible implementation. – Michał Ziober Nov 22 '19 at 21:52
  • yes I am looking into providing an implementation of BooleanDeserializers class that checks this flag. Will send a PR if I am able to do it – ghanekar.omkar Nov 25 '19 at 22:26