jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Deserialization of 2D arrays of final types, when using `DefaultTyping.NON_FINAL`

Open klaasdellschaft opened this issue 3 years ago • 2 comments

Hi,

I'm currently trying to serialize and deserialize a two-dimensional array of final types that are stored in a two-dimensional array of a non-final superclass (in the example below: storing a value of String[][] in a field of type Object[][]). In order to be able to reconstruct the type information, I activated default typing with DefaultTyping.NON_FINAL. However, during deserialization Jackson fails with com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type 'java.lang.String' from Array value (token 'JsonToken.START_ARRAY').

Following test case:

private static final class SomeBean {
    public Object[][] value;
}

public void testTwoDimensionalArrayMapping() throws JsonProcessingException {
    PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
            .allowIfSubTypeIsArray()
            .allowIfSubType(Object.class)
            .build();

    ObjectMapper mapper = JsonMapper
            .builder()
            .activateDefaultTyping(typeValidator, NON_FINAL)
            .build();

    SomeBean instance = new SomeBean();

    // case 1 - successful
    instance.value = new Object[][]{new String[]{"1.1", "1.2"}, new String[]{"2.1", "2.2"}};
    String serialized = mapper.writeValueAsString(instance);
    SomeBean deserialized = mapper.readValue(serialized, SomeBean.class); // successful
    assertEquals(Object[][].class, deserialized.value.getClass());
    assertEquals(String[].class, deserialized.value[0].getClass());

    // case 2 - successful
    instance.value = new Object[][]{{"1.1", "1.2"}, {"2.1", "2.2"}};
    serialized = mapper.writeValueAsString(instance);
    deserialized = mapper.readValue(serialized, SomeBean.class); // successful
    assertEquals(Object[][].class, deserialized.value.getClass());
    assertEquals(Object[].class, deserialized.value[0].getClass());

    // case 3 (handcrafted JSON) - successful
    String handcrafted = "{\"value\":[\"[[Ljava.lang.String;\",[[\"1.1\",\"1.2\"],[\"2.1\",\"2.2\"]]]}";
    deserialized = mapper.readValue(handcrafted, SomeBean.class); // successful
    assertEquals(String[][].class, deserialized.value.getClass());
    assertEquals(String[].class, deserialized.value[0].getClass());

    // case 4 - fails
    instance.value = new String[][]{{"1.1", "1.2"}, {"2.1", "2.2"}};
    serialized = mapper.writeValueAsString(instance);
    deserialized = mapper.readValue(serialized, SomeBean.class); // fails
    assertEquals(String[][].class, deserialized.value.getClass());
    assertEquals(String[].class, deserialized.value[0].getClass());
}

The serialized strings are as follows:

  1. Successful: {"value":["[[Ljava.lang.Object;",[["[Ljava.lang.String;",["1.1","1.2"]],["[Ljava.lang.String;",["2.1","2.2"]]]]}
  2. Successful: {"value":["[[Ljava.lang.Object;",[["[Ljava.lang.Object;",["1.1","1.2"]],["[Ljava.lang.Object;",["2.1","2.2"]]]]}
  3. Successful (handcrafted JSON): {"value":["[[Ljava.lang.String;",[["1.1","1.2"],["2.1","2.2"]]]}
  4. Fails: {"value":["[[Ljava.lang.String;",[["[Ljava.lang.String;",["1.1","1.2"]],["[Ljava.lang.String;",["2.1","2.2"]]]]}

As far as I understand it, the problem is that the serializer and the deserializer are looking at different types in order to find out whether they are currently handling a non-final type:

  • In case of the serializer, it looks at the declared type of the variable (i.e. Object[][]) for the outer array as well as the inner array. Thus, it is adding the type id for outer and inner array, even if the actual type of the outer array is final, and thus also the inner array must be final.
  • In case of the deserializer, it looks at the actual type of the outer array (i.e. String[][]) in order to find out that the inner array must also be final. Thus, it is failing when it actually encounters the type id / the actual inner string array added by the serializer. This is also confirmed by the handcrafted JSON, where I removed the type information from the inner array. In that case, the deserialization is successful.

Further information:

  • The problem not only exists for String[][] but also for other two-dimensional arrays of final types.
  • Using DefaultTyping.EVERYTHING is a successful workaround.

Full stack trace of the error:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.lang.String` from Array value (token `JsonToken.START_ARRAY`)
 at [Source: (String)"{"value":["[[Ljava.lang.String;",[["[Ljava.lang.String;",["1.1","1.2"]],["[Ljava.lang.String;",["2.1","2.2"]]]]}"; line: 1, column: 58] (through reference chain: com.fasterxml.jackson.databind.introspect.TwoDimensionalGenericArraysTest$SomeBean["value"]->java.lang.Object[][0]->java.lang.Object[][1])

	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
	at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1741)
	at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1515)
	at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1420)
	at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseString(StdDeserializer.java:1299)
	at com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer.deserialize(StringArrayDeserializer.java:166)
	at com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer.deserialize(StringArrayDeserializer.java:25)
	at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:214)
	at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:24)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:120)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:53)
	at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserializeWithType(ObjectArrayDeserializer.java:246)
	at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserializeWithType(ObjectArrayDeserializer.java:24)
	at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:147)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:313)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:176)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4620)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3575)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3543)
	at com.fasterxml.jackson.databind.introspect.TwoDimensionalGenericArraysTest.testTwoDimensionalArrayMapping(TwoDimensionalGenericArraysTest.java:56)

klaasdellschaft avatar Jun 30 '21 10:06 klaasdellschaft