3

Facing issue while moving my project from Jersey to Spring MVC. How to relax case insensitivity for root value in Jackson?

I would like to support both upper and lower case.

We have below Jackson configurations which works fine for properties and enums but not for root value

spring.jackson.mapper.accept-case-insensitive-properties=true
spring.jackson.mapper.accept-case-insensitive-enums=true

In my case, I do not have access to Car class and hence can not use any of the Jackson annotations like @JsonRootValue to update the name of Car class to 'car'

Here is the sample class with test to reproduce the issue, I am facing.

Wouldn't it be nice to have configuration to relax root name in jackson library?

public class CarTest {

    public class Car {
        String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
    
    @Test
    public void testCarRootValueCaseSensitivity() throws IOException {
        Car car = new Car();
        car.setName("audi");

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
        objectMapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
        String carAsString = objectMapper.writeValueAsString(car);
        System.out.println(carAsString);

        // Works fine with out any exception as the root value Car has captital 'C'
        objectMapper.readValue("{\"Car\":{\"name\":\"audi\"}}", Car.class);

        // Throws exception when lower case 'c' is provided than uppercase 'C'
        objectMapper.readValue("{\"car\":{\"name\":\"audi\"}}", Car.class);
    }
}
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • You need to use objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); – Ganesh chaitanya Jul 29 '20 at 20:44
  • @Ganeshchaitanya MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES works for property values but not for root value – TechLearning Jul 29 '20 at 20:59
  • May be you should you objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true); objectMapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true); – Ganesh chaitanya Jul 29 '20 at 21:01
  • @Ganeshchaitanya Problem is not with un wrapping, problem id with case sensitivity for root value. – TechLearning Jul 29 '20 at 21:08
  • You can try to use `MixIn` feature and declare root type name using extra class or interface. Take a look on examples: [Jackson conditional @JsonUnwrapped](https://stackoverflow.com/questions/25425419/jackson-conditional-jsonunwrapped/25435990#25435990), [Custom Jackson Serializer for a specific type in a particular class](https://stackoverflow.com/questions/23252918/custom-jackson-serializer-for-a-specific-type-in-a-particular-class/23255539#23255539) – Michał Ziober Jul 29 '20 at 21:24
  • 1
    @MichałZiober In my case, I want to support both cases. If I use MixIn, I will not be able to support both – TechLearning Jul 29 '20 at 22:05
  • Which version of `Jackson` do you use? – Michał Ziober Jul 29 '20 at 22:30
  • I am using 2.10.1 – TechLearning Jul 29 '20 at 22:31

1 Answers1

1

When you take a look on exception which is thrown:

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Root name 'car' does not match expected ('Car') for type [simple type, class com.example.Car]
 at [Source: (String)"{"car":{"name":"audi"}}"; line: 1, column: 2] (through reference chain: com.example.Car["car"])
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
    at com.fasterxml.jackson.databind.DeserializationContext.reportPropertyInputMismatch(DeserializationContext.java:1477)
    at com.fasterxml.jackson.databind.DeserializationContext.reportPropertyInputMismatch(DeserializationContext.java:1493)
    at com.fasterxml.jackson.databind.ObjectMapper._unwrapAndDeserialize(ObjectMapper.java:4286)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4200)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3205)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3173)

you can notice _unwrapAndDeserialize method name.

In this method you can find this piece of code:

String actualName = p.getCurrentName();
if (!expSimpleName.equals(actualName)) {
   ctxt.reportPropertyInputMismatch(rootType, actualName,
       "Root name '%s' does not match expected ('%s') for type %s",
       actualName, expSimpleName, rootType);
}

There is no way to configure this behaviour since always equals method is used.

If you really want to parse it with case insensitive mode you can override _unwrapAndDeserialize method and replace equals with equalsIgnoreCase. Example class:

class CaseInsensitiveObjectMapper extends ObjectMapper {
    protected Object _unwrapAndDeserialize(JsonParser p, DeserializationContext ctxt, DeserializationConfig config, JavaType rootType, JsonDeserializer<Object> deser) throws IOException {
        PropertyName expRootName = config.findRootName(rootType);
        // 12-Jun-2015, tatu: Should try to support namespaces etc but...
        String expSimpleName = expRootName.getSimpleName();
        if (p.getCurrentToken() != JsonToken.START_OBJECT) {
            ctxt.reportWrongTokenException(rootType, JsonToken.START_OBJECT,
                    "Current token not START_OBJECT (needed to unwrap root name '%s'), but %s",
                    expSimpleName, p.getCurrentToken());
        }
        if (p.nextToken() != JsonToken.FIELD_NAME) {
            ctxt.reportWrongTokenException(rootType, JsonToken.FIELD_NAME,
                    "Current token not FIELD_NAME (to contain expected root name '%s'), but %s",
                    expSimpleName, p.getCurrentToken());
        }
        String actualName = p.getCurrentName();
        if (!expSimpleName.equalsIgnoreCase(actualName)) {
            ctxt.reportPropertyInputMismatch(rootType, actualName,
                    "Root name '%s' does not match expected ('%s') for type %s",
                    actualName, expSimpleName, rootType);
        }
        // ok, then move to value itself....
        p.nextToken();
        Object result = deser.deserialize(p, ctxt);
        // and last, verify that we now get matching END_OBJECT
        if (p.nextToken() != JsonToken.END_OBJECT) {
            ctxt.reportWrongTokenException(rootType, JsonToken.END_OBJECT,
                    "Current token not END_OBJECT (to match wrapper object with root name '%s'), but %s",
                    expSimpleName, p.getCurrentToken());
        }
        if (config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
            _verifyNoTrailingTokens(p, ctxt, rootType);
        }
        return result;
    }
}

I copied the whole method from ObjectMapper class (version 2.10.1) and in case you will upgrade Jackson version you need to check how implementation of this method looks like and replace it if needed.

Finally, you can use this new type in your test:

ObjectMapper objectMapper = new CaseInsensitiveObjectMapper();

See also:

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • This one is working fine but how ever I now have issues setting this new ObjectMapper globally and replace the default ObjectMapper. – TechLearning Jul 30 '20 at 04:18
  • @TechLearning, take a look at [Configuring ObjectMapper in Spring](https://stackoverflow.com/questions/7854030/configuring-objectmapper-in-spring), [SpringBoot: Consume & Produce XML with a Custom Serializer + Deserializer](https://stackoverflow.com/questions/58184442/springboot-consume-produce-xml-with-a-custom-serializer-deserializer) – Michał Ziober Jul 30 '20 at 07:39
  • Thanks for the pointers. Your solution is working fine. – TechLearning Jul 30 '20 at 16:04
  • @TechLearning, I'g glad to hear that. [How does accepting an answer work?](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work) – Michał Ziober Jul 30 '20 at 17:22