jackson-databind
jackson-databind copied to clipboard
Deserialization of 2D arrays of final types, when using `DefaultTyping.NON_FINAL`
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:
- Successful:
{"value":["[[Ljava.lang.Object;",[["[Ljava.lang.String;",["1.1","1.2"]],["[Ljava.lang.String;",["2.1","2.2"]]]]}
- Successful:
{"value":["[[Ljava.lang.Object;",[["[Ljava.lang.Object;",["1.1","1.2"]],["[Ljava.lang.Object;",["2.1","2.2"]]]]}
- Successful (handcrafted JSON):
{"value":["[[Ljava.lang.String;",[["1.1","1.2"],["2.1","2.2"]]]}
- 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)
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
.
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.