Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I deserialize to Boolean.class from Json object in case insensitive manner using Jackson?

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] 
like image 610
ghanekar.omkar Avatar asked Nov 22 '19 18:11

ghanekar.omkar


1 Answers

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}
like image 194
Michał Ziober Avatar answered Oct 03 '22 15:10

Michał Ziober