DominoKit / domino-jackson

Jackson with Annotation processing
Apache License 2.0
53 stars 14 forks source link

In some cases @JsonSubTypes leads to IndexOutOfBoundsException #38

Closed treblereel closed 4 years ago

treblereel commented 4 years ago

This Exception occurs during the generation phase, processing this mapping configuration:

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true)
    @JsonSubTypes({
            @JsonSubTypes.Type(value = DataInput.class, name = "dataInput"),
            @JsonSubTypes.Type(value = DataOutput.class, name = "dataOutput"),
    })
    private List<Data> ioSpecification;

public abstract class Data<T extends Data> {}
public class DataInput extends Data<DataInput> {}
public class DataOutput extends Data<DataOutput> {}

Looks like the generator isn't happy about Data so it fails. In my humble opinion, it can be fixed by skipping analysis of Data, coz it's redundant for collections annotated with @JsonSubTypes where end-user is responsible which children of Data they want to store in this collection.

Here is the reproducer: https://github.com/treblereel/j2cl-tests/tree/domino-json

vegegoku commented 4 years ago

I have been investigating this for some time and these are my findings :

private List<Data<?>> ioSpecification;

And for testing

String typeErrs = res.entrySet().stream()
                .filter(entry -> Type.hasTypeArgumentWithBoundedWildcards(entry.getValue()) || Type.hasUnboundedWildcards(entry.getValue()))
                .map(entry -> "Member '" + entry.getKey().getSimpleName() + "' resolved type: '" + entry.getValue() + "'")
                .collect(Collectors.joining("\n"));

        if (!typeErrs.isEmpty())
            throw new RuntimeException(
                    "Type: '" + enclosingType
                            + "' could not have generic member of type parametrized with type argument having unbounded wildcards"
                            + " or non-collections having type argument with bounded wildcards:\n"
                            + typeErrs);
            if (
                    !((DeclaredType)beanType).getTypeArguments().isEmpty()
                    || !((DeclaredType)((DeclaredType)subtypeEntry.getValue()).asElement().asType()).getTypeArguments().isEmpty())
                throw new RuntimeException("@JsonSubTypes and &JsonTypeInfo can be used only on non-generic Java types");

The pojos i am testing with now looks like this :

public abstract class Data<T extends Data> {

    protected String id;

    protected String dtype;

    protected String itemSubjectRef;

    protected String name;

    public String getDtype() {
        return dtype;
    }

    public T setDtype(String dtype) {
        this.dtype = dtype;
        return (T) this;
    }

    public String getId() {
        return id;
    }

    public T setId(String id) {
        this.id = id;
        return (T) this;
    }

    public String getItemSubjectRef() {
        return itemSubjectRef;
    }

    public T setItemSubjectRef(String itemSubjectRef) {
        this.itemSubjectRef = itemSubjectRef;
        return (T) this;
    }

    public String getName() {
        return name;
    }

    public T setName(String name) {
        this.name = name;
        return (T) this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Data<?> data = (Data<?>) o;
        return Objects.equals(id, data.id) &&
                Objects.equals(dtype, data.dtype) &&
                Objects.equals(itemSubjectRef, data.itemSubjectRef) &&
                Objects.equals(name, data.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, dtype, itemSubjectRef, name);
    }
}

public class DataInput extends Data<DataInput> {

    public DataInput() {

    }

    public DataInput(String id, String postfix, String name, String dtype) {
        this(id, postfix, name);
        this.dtype = dtype;
    }

    public DataInput(String id, String postfix, String name) {
        this.id = id + "_" + postfix;
        this.itemSubjectRef = id + "_" + postfix + "Item";
        this.name = name;
    }
}
public class DataOutput extends Data<DataOutput> {

    public DataOutput() {

    }

    public DataOutput(String id, String postfix, String name) {
        this.id = id + "_" + postfix;
        this.itemSubjectRef = id + "_" + postfix + "Item";
        this.name = name;
    }
}
@JSONMapper
public class DataList {

    private List<Data<?>> ioSpecification;

    public List<Data<?>> getIoSpecification() {
        return ioSpecification;
    }

    public void setIoSpecification(List<Data<?>> ioSpecification) {
        this.ioSpecification = ioSpecification;
    }
}

And I have the following test case :


public class InheritanceWithJsonTypeInfoAndGenericsTest {

    @Test
    public void serializerTest(){

        DataInput dataInput = new DataInput("1", "diPostfix", "diName", "diDType");
        DataOutput dataOutput = new DataOutput("2", "doPostfix", "doName");

        DataList dataList = new DataList();
        ArrayList<Data<?>> ioSpecification = new ArrayList<>();
        ioSpecification.add(dataInput);
        ioSpecification.add(dataOutput);
        dataList.setIoSpecification(ioSpecification);

        String result = DataList_MapperImpl.INSTANCE.write(dataList);
        Assert.assertEquals("{\"ioSpecification\":[{\"@type\":\"dataInput\",\"id\":\"1_diPostfix\",\"dtype\":\"diDType\",\"itemSubjectRef\":\"1_diPostfixItem\",\"name\":\"diName\"},{\"@type\":\"dataOutput\",\"id\":\"2_doPostfix\",\"dtype\":null,\"itemSubjectRef\":\"2_doPostfixItem\",\"name\":\"doName\"}]}",
                result);
    }

    @Test
    public void deserializerTest(){

        String json = "{\"ioSpecification\":[{\"@type\":\"dataInput\",\"id\":\"1_diPostfix\",\"dtype\":\"diDType\",\"itemSubjectRef\":\"1_diPostfixItem\",\"name\":\"diName\"},{\"@type\":\"dataOutput\",\"id\":\"2_doPostfix\",\"dtype\":null,\"itemSubjectRef\":\"2_doPostfixItem\",\"name\":\"doName\"}]}";
        DataList result = DataList_MapperImpl.INSTANCE.read(json);

        DataInput dataInput = new DataInput("1", "diPostfix", "diName", "diDType");
        DataOutput dataOutput = new DataOutput("2", "doPostfix", "doName");

        Assert.assertEquals(result.getIoSpecification().size(),2);
        Assert.assertTrue(result.getIoSpecification().get(0) instanceof DataInput);
        Assert.assertTrue(result.getIoSpecification().get(1) instanceof DataOutput);
        Assert.assertEquals(dataInput, result.getIoSpecification().get(0));
        Assert.assertEquals(dataOutput, result.getIoSpecification().get(1));
    }
}

With these modifications, the test and all other tests in the library still works.

But there were few things I noticed,

this issue appears more clearly if we define a generic field in the Data class for example. where the Data class does not define the actual type for the field but the subclasses do, so the subclasses (De)serializers are generated correctly.

So here I think for abstract classes no fields serializers should be generated.

@tedynaidenov sorry to bother you, but do you have feedback on this?.

vegegoku commented 4 years ago

In case we need to have more investigation or we found that we have to handle more weird use cases we can open new issues or reopen this one.

tedynaidenov commented 4 years ago

Sorry for getting to this so late. It was too long ago to remember details, but I think the problem with bounded wildcards was the type of parameter for setValue() and type of the result for getValue() in the corresponding serializer/deserializer. I tried to use the upper bound, but it didn't work well. Probably we have plenty of cases, where this limitation can be relaxed.