Closed treblereel closed 4 years ago
I have been investigating this for some time and these are my findings :
The annotations does not work on fields but rather on the base type, I moved
@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"),
})
To the Data
class instead of the field.
The processor will try to match the Type arguments from the base class with the type arguments of the field, and the number of arguments should match so I changed the field as follows:
private List<Data<?>> ioSpecification;
And for testing
AbstractJsonMapperGenerator
I removed this check which prevents using a wildcard in a base class.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);
SerializerGenerator
and DeserializerGenerator
we have the following check which I also removed 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,
Data
class are generated with the (De)serializers for each field. this would be true if the class not abstract and not annotated with the @JsonTypeInfo
and @JsonSubTypes
, but for an abstract class that defines its possible subtypes there should be no fields (De)serializers to be generated since the mapping will happen with one of the subtypes.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.
The other thing is if add a field in the base class with a generic type where the base class isn't abstract, is it possible that we generate proper (De)serializers for that field.?
Last why do we ban all uses of a wildcard when it is possible to support that in some cases, and why not to leave this validation to the compiler, since the compiler will complain from the generated code when we don't something not supported?
@tedynaidenov sorry to bother you, but do you have feedback on this?.
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.
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.
This Exception occurs during the generation phase, processing this mapping configuration:
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