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

@JsonTypeInfo with EXTERNAL_PROPERTY doesn't handle arrays of polymorphic types

Open alex-crowell opened this issue 8 years ago • 7 comments

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!

alex-crowell avatar Feb 12 '16 17:02 alex-crowell

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!

cowtowncoder avatar Feb 12 '16 17:02 cowtowncoder

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.

alex-crowell avatar Feb 12 '16 18:02 alex-crowell

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"
    }
}

alex-crowell avatar Feb 12 '16 18:02 alex-crowell

May I know is there any workaround for this?

sanintel3 avatar Mar 05 '18 09:03 sanintel3

I am interested in any workaround too :)

EmhyrVarEmreis avatar Jul 17 '21 18:07 EmhyrVarEmreis

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 avatar Apr 12 '22 17:04 msmerc

@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.

cowtowncoder avatar Apr 19 '22 17:04 cowtowncoder