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 4 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

Ok I'll have to look into this a bit.

As to ser/deser discrepancy; this should not directly affect Type Id (both are determined based on declared type, not actual runtime type). Only content serialization uses actual type (deserialization must rely on declared type, or for polymorphic handling, type id).

But there definitely seems to be some odd difference in which deserialization does not expect to see inner type. That is likely related to difference between final String and non-final Object.

cowtowncoder avatar Aug 14 '21 20:08 cowtowncoder

Looks like TypeDeserializer for outer ObjectArrayDeserializer remains null which explains why Type Id is not expected -- and since Type Id encapsulation for arrays is JSON Array, this leads to "unexpected START_ARRAY" in place where the first String element is expected.

So the question is if and how to sync ser/deser sides: either to drop Type Ids that are unnecessary, on serialization; or expect them on deserialization side.

I don't think I have time to work on this much further at this point unfortunately, but I will add a failing test included.

A work-around for now (aside from using EVERYTHING) would be to create a custom StdTypeResolverBuilder but that does get complicated and is not well documented.

cowtowncoder avatar Aug 14 '21 21:08 cowtowncoder