jackson-databind
jackson-databind copied to clipboard
@JsonTypeInfo with EXTERNAL_PROPERTY doesn't handle arrays of polymorphic types
While the JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY works fine normally, there is an issue when using it along with @JsonTypeInfo and its JsonTypeInfo.As.EXTERNAL_PROPERTY feature (regular PROPERTY works fine). Below is an example test case to illustrate the issue:
public static class Foo {
public String msg;
}
public static class FooA extends Foo {
public String hey;
}
public static class FooB extends Foo {
public String whoa;
}
public static class Holder {
public String footype;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "footype")
@JsonSubTypes({@JsonSubTypes.Type(value=FooA.class, name="a"),
@JsonSubTypes.Type(value=FooB.class, name="b") })
@JsonFormat(with=JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
public List<Foo> foo;
public Foo other;
}
@Test
public void testSVA() throws Exception
{
final InputStream stream = TestSingleValueArray.class.getResourceAsStream("footest.json");
Scanner s = new Scanner(stream);
final String json = s.useDelimiter("\\Z").next();
s.close();
final ObjectMapper mapper = new ObjectMapper();
final Holder holder = mapper.readValue(json, Holder.class);
}
I expected the above test to combine the external property and array features, but it instead throws the following error:
com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class TestSingleValueArray$Foo
at [Source: {
"footype": "a",
"foo": {
"msg": "Hello World",
"hey": "there"
},
"other": {
"msg": "Goodbye"
}
}; line: 3, column: 12] (through reference chain: Holder["foo"]->java.lang.Object[0])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:216)
at com.fasterxml.jackson.databind.DeserializationContext.wrongTokenException(DeserializationContext.java:962)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._locateTypeId(AsArrayTypeDeserializer.java:127)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:93)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromObject(AsArrayTypeDeserializer.java:58)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1017)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.handleNonArray(CollectionDeserializer.java:341)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:259)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:490)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:101)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:260)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:125)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3788)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2779)
at TestSingleValueArray.testSVA(TestSingleValueArray.java:63)
Here is the input used to produce the above error:
{
"footype": "a",
"foo": {
"msg": "Hello World",
"hey": "there"
},
"other": {
"msg": "Goodbye"
}
}
Moving the footype field into Foo and switching from EXTERNAL_PROPERTY to a plain PROPERTY fixes the error, but of course is not quite the same.
Thanks in advance for your help!
Thank you for reporting this. When combinations of features get more complicated there are bound to be edge cases, so I can not say in advance how easy this will be to fix; I hope it turns out to be easy. Thank you for contributing the test case!
Thanks for your quick response!
The more I'm looking at this, it's looking like the problem is that the JsonSubTypes feature might be having trouble working with lists in general, and (using my example) there may be some confusion in Jackson about whether to expect a List<Foo> or a FooA. It doesn't appear to be limited to using ACCEPT_SINGLE_VALUE_AS_ARRAY, the problem is more generally that Jackson doesn't seem to support using EXTERNAL_PROPERTY JsonSubTypes with arrays of those subtype objects.
The more general error can be reproduced by removing the @JsonFormat annotation above the foo field in the Java code (just 1 line) and converting "foo" to hold an array object in the JSON input file (as shown below). It appears to produce the exact same error as before.
{
"footype": "a",
"foo": [ {
"msg": "Hello World",
"hey": "there"
} ],
"other": {
"msg": "Goodbye"
}
}
May I know is there any workaround for this?
I am interested in any workaround too :)
So it seems the first problem is that collection / list properties annotated this way aren't picked up and included in the _externalTypeHnadler
because they don't have a valueTypeDeserialiser
. See https://github.com/FasterXML/jackson-databind/blob/92f809dd161a80d0d5c519f78ea18443715970ad/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java#L592
Though I'm guessing this is just the tip of the iceberg and there would be more work after this.
@cowtowncoder Might it be possible to get some love for this issue? I'm guessing it's unlikely!
@msmerc Unfortunately I don't think I will have time to dig into this issue, although ideally would of course love to help.
The big problem, I think, is that EXTERNAL_PROPERTY
basically CANNOT work for "real" arrays (that is, Collections
and arrays serialized as JSON Arrays) because there is no place in JSON Array to add properties. It could, in theory, work for the special case of "unwrapped" single-element array, but that seems fragile as it would only work for that one special case.
I think that one good improvement would be for Jackson to detect this usage attempt at fail immediately with proper exception message (cannot use this combination of features), to at least let user know what is the issue.