Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonTypeInfo with a subtype having JsonFormat.Shape.ARRAY and no fields generates {} not [] #2077

Closed
frsyuki opened this issue Jun 28, 2018 · 5 comments
Milestone

Comments

@frsyuki
Copy link

frsyuki commented Jun 28, 2018

When a polymorphic interface has a subtype, and the subtype has @JsonFormat(shape=JsonFormat.Shape.ARRAY) annotation and no fields, serializing its instance generates an empty object ({}). However, parsing the interface tries to parse it as an array and fails.

Expected result is that it generates an empty array ([]) instead of object so that parsing succeeds.

Here is a code to reproduce this issue:

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.List;

public class JacksonArrayTest
{
    @JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.WRAPPER_ARRAY)  // Both WRAPPER_OBJECT and WRAPPER_ARRAY cause the same problem
    @JsonSubTypes({
        @JsonSubTypes.Type(value = DirectLayout.class, name = "Direct"),
    })
    public interface Layout {
    }

    @JsonFormat(shape=JsonFormat.Shape.ARRAY)
    public static class DirectLayout implements Layout {
    }

    public static void main(String[] args) throws Exception {
        ObjectMapper om = new ObjectMapper();

        String json = om.writeValueAsString(new DirectLayout());
        System.out.println("Serialized JSON: " + json);  //=> ["Direct",{}]. This is expected to be ["Direct",[]]

        Layout instance = om.readValue(json, Layout.class);
        System.out.println("Deserialized object class: " + instance.getClass());  //=> fails
    }
}

Stacktrace of the failure is this:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize a POJO (of type JacksonArrayTest$DirectLayout) from non-Array representation (token: START_OBJECT): type/property designed to be serialized as JSON Array
 at [Source: ["Direct",{}]; line: 1, column: 11]
        at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
        at com.fasterxml.jackson.databind.DeserializationContext.reportMappingException(DeserializationContext.java:1234)
        at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1122)
        at com.fasterxml.jackson.databind.deser.impl.BeanAsArrayDeserializer._deserializeFromNonArray(BeanAsArrayDeserializer.java:352)
        at com.fasterxml.jackson.databind.deser.impl.BeanAsArrayDeserializer.deserialize(BeanAsArrayDeserializer.java:97)
        at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:116)
        at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromObject(AsArrayTypeDeserializer.java:61)
        at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:209)
        at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:63)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3814)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2858)
        at JacksonArrayTest.main(JacksonArrayTest.java:49)

This problem reproduces with Jackson 2.8.11 and 2.9.6.

This problem doesn't happen if the subtype has at least one field like this:

    @JsonFormat(shape=JsonFormat.Shape.ARRAY)
    public static class DirectLayout implements Layout {
        public void setDummy(int v) {
        }

        public int getDummy() {
            return 0;
        }
    }

    public static void main(String[] args) throws Exception {
        ObjectMapper om = new ObjectMapper();

        String json = om.writeValueAsString(new DirectLayout());
        System.out.println("Serialized JSON: " + json);  //=> ["Direct",[0]]

        Layout instance = om.readValue(json, Layout.class);
        System.out.println("Deserialized object class: " + instance.getClass());  //=> class JacksonArrayTest$DirectLayout
    }

A workaround is adding @JsonValue method to the subtype as following:

    @JsonFormat(shape=JsonFormat.Shape.ARRAY)
    public static class DirectLayout implements Layout {
        @JsonValue
        @Deprecated
        public List<Integer> toJson()
        {
            return new ArrayList<>();
        }
    }

    public static void main(String[] args) throws Exception {
        ObjectMapper om = new ObjectMapper();

        String json = om.writeValueAsString(new DirectLayout());
        System.out.println("Serialized JSON: " + json);  //=> ["Direct",[]]. This is expected.

        Layout instance = om.readValue(json, Layout.class);
        System.out.println("Deserialized object class: " + instance.getClass());  //=> class JacksonArrayTest$DirectLayout
    }
@cowtowncoder
Copy link
Member

This combination is unlikely to be supported due to limitations of JSON so while I will not close this yet, I would strongly suggest that you will try to find a solution where subtypes do not use different shapes.
I don't think this is necessarily something that we could support even theoretically.

@cowtowncoder
Copy link
Member

Reading the problem description again, this is intriguing actually... so will try to have another look.
Shape should not vary even if no properties are found, so maybe this is actually due to special serialized used by empty case.

@cowtowncoder
Copy link
Member

Ah. This is because value being serialized is a "root value" -- yet another reason why I think root values should always be simple wrappers themselves. Looks like class annotations not resolved in that case, because BeanProperty does not exist....

cowtowncoder added a commit that referenced this issue Sep 21, 2019
@cowtowncoder
Copy link
Member

So: interesting that deserializer does get annotation correctly, expects array shape, but serializer not. Correct solution would be to make sure class annotations checked, possibly format settings passed during initial construction but applied during contextualization.

@cowtowncoder
Copy link
Member

Oh. Except that part of the issue is that there are no properties (like mentioned): so only "dummy" serializer is created.
Also, BeanSerializer[Base] actually DOES have _serializationShape

@cowtowncoder cowtowncoder added this to the 2.10.0 milestone Sep 21, 2019
@cowtowncoder cowtowncoder changed the title JsonTypeInfo with a subtype having JsonFormat.Shape.ARRAY and no fields generates {} but tries to parse [] and fails JsonTypeInfo with a subtype having JsonFormat.Shape.ARRAY and no fields generates {} not [] Sep 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants