英文:
Mapping Typescript dynamic types into Java Object's subclasses
问题
我需要创建一个Jackson映射并使用Java的强大instanceof
运算符来处理反序列化类。看看这个目标模型:
@Data
public class Field {
private DetFieldAlignment align; // css property that does not work disabled: boolean;
....
private String optional;// just in 3 fields, never used
private Object option; // 'plugin:...' | [{ value: 1, label: 'my value' }, ...] | { "1": "Option one ..."}
}
@Data
public class DetFieldOption {
private String value;
private String label;
private String disable;
....
}
我需要指示Jackson对Object
的可能子类进行多态序列化,但我遇到了困难。我编写了JUnit测试来验证结果(option instanceof String
与检查列表中的每个项目是否instanceof FieldOption
),但显然它不起作用。
使用上述模型,我的测试负载中识别为String
,但FieldOption
数组被翻译为LinkedHashMap
数组。
在尚未处理Map<Integer,String>
情况之前,我尝试向Object option
字段添加以下多态注释:
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
@JsonSubTypes.Type(String.class),
@JsonSubTypes.Type(FieldOption.class)
})
这导致以下异常:
Caused by: java.lang.IllegalStateException: Subtypes java.lang.String and java.lang.Object have the same signature and cannot be uniquely deduced.
at com.fasterxml.jackson.databind.jsontype.impl.AsDeductionTypeDeserializer.buildFingerprints(AsDeductionTypeDeserializer.java:89)
at com.fasterxml.jackson.databind.jsontype.impl.AsDeductionTypeDeserializer.<init>(AsDeductionTypeDeserializer.java:48)
at com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder.buildTypeDeserializer(StdTypeResolverBuilder.java:166)
at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findPropertyTypeDeserializer(BasicDeserializerFactory.java:2022)
... 89 more
我无法控制来自前端的负载,这意味着我无法要求前端开发人员在负载中添加类型指示,尤其是考虑到对象可能是字符串。
问题
如何定义一个多态序列化,可以反序列化为以下之一:
- java.lang.String
- java.util.Collection<FieldOption>
- java.util.Map<Integer,String>
当然,我不期望键除了整数之外还有其他类型。
英文:
I have a Typescript model being sent as JSON request to my back end server. I can't change the model into a more engineered format, so I have to use what the client is going to send me.
The Typescript interface uses the pipe to perform polymorphism as shown below:
export interface Field {
align: 'center' | 'left'; // css property that does not work disabled: boolean;
label: string;
readonly: boolean;
ref: string; // unique identifier size: string; // css width type: FIELD_TYPES;
value: string;
required: boolean;
visible: string; // 'true' | 'plugin:...'
invisible: 'true' | 'false'; // never used
optional: string; // just in 3 fields, never used
option:
| string // 'plugin:...'
| { value: string; label: string; disable?: string; labelEng?: string, valueOfAnswerInInterestType?: string }[] //FieldOption
| { 1?: string; 2?: string; 3?: string; 4?: string; 5?: string; 6?: string }; //Map<Integer,String>
}
As you can see, option
can either be String
or an array, or a plain POJO.
What I need to achieve is to create a Jackson mapping and use Java's powerful instanceof
operator to work on the deserialized class.
Look at this target model:
@Data
public class Field {
private DetFieldAlignment align; // css property that does not work disabled: boolean;
....
private String optional;// just in 3 fields, never used
private Object option; // 'plugin:...' | [{ value: 1, label: 'my value' }, ...] | { "1": "Option one ..."}
}
@Data
public class DetFieldOption {
private String value;
private String label;
private String disable;
....
}
I need to instruct Jackson about polymorphic serialization for the possible subclasses of Object
, but I am having difficulty.
I wrote JUnit tests to validate the result (option instanceof String
vs check that every item in list is instanceof FieldOption
) but obviously it's not working.
With the above model, a String
is recognized in my test payload, but the array of FieldOption
translated into an array of LinkedHashMap
s
Without yet working on the case of Map<Integer,String>
, I have tried to add the following polymorphic annotations to Object option
field
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
@JsonSubTypes.Type(String.class),
@JsonSubTypes.Type(FieldOption.class)
})
It does not work with the following exception
Caused by: java.lang.IllegalStateException: Subtypes java.lang.String and java.lang.Object have the same signature and cannot be uniquely deduced.
at com.fasterxml.jackson.databind.jsontype.impl.AsDeductionTypeDeserializer.buildFingerprints(AsDeductionTypeDeserializer.java:89)
at com.fasterxml.jackson.databind.jsontype.impl.AsDeductionTypeDeserializer.<init>(AsDeductionTypeDeserializer.java:48)
at com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder.buildTypeDeserializer(StdTypeResolverBuilder.java:166)
at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findPropertyTypeDeserializer(BasicDeserializerFactory.java:2022)
... 89 more
I can't control the payload coming from the front end, which means I can't ask the FE developers to add a type indication in the payload, especially considering that the object could be a String.
Question
How can I define a polymorphic serialization that deserializes into either:
- java.lang.String
- java.util.Collection<FieldOption>
- java.util.Map<Integer,String>
Of course, I don't expect keys to be other than integers
答案1
得分: 2
因为您想要将一个多态值封装到您的Field
类的Object
字段中,这个任务可以通过一个自定义的StdDeserializer
反序列化器来实现,该反序列化器可以区分您指定的三种情况(字符串,数组和映射)。因此,如果您采取您的类的简化版本如下:
@Data
public class Field {
@JsonDeserialize(using = OptionDeserializer.class)
private Object option;
}
@Data
public class DetFieldOption {
private String value;
private String label;
}
您可以定义一个自定义的stdDeserializer
,可以在您指定的三种情况下返回不同的对象,检查JsonNode
选项是字符串、数组还是对象:
public class OptionDeserializer extends StdDeserializer<Object> {
public OptionDeserializer() {
super(Object.class);
}
@Override
public Object deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
ObjectCodec codec = jp.getCodec();
JsonNode node = codec.readTree(jp);
// jsonnode是一个数组,所以它将被转换为DetFieldOption[]
if (node.isArray()) {
return codec.treeToValue(node, DetFieldOption[].class);
}
// jsonnode是一个POJO,所以要将它转换为Map<String, String>
// 需要定义一个新的TypeReference
// 没有键为Integer,因为在JSON中键始终是字符串
// 但是如果需要,您可以将它们转换为整数
if (node.isObject()) {
TypeReference<Map<String, String>> typeRef = new TypeReference<Map<String, String>>() {};
Map<String, String> map = codec.readValue(codec.treeAsTokens(node),
dc.getTypeFactory().constructType(typeRef));
return map;
}
// 默认情况下,node是一个字符串
return node.asText();
}
}
然后,您可以将类似下面的JSON字符串转换为Field
对象:
String json1 = "{\"option\": \"myString\"}";
String json2 = "{\"option\": [{\"value\": \"myValue\"}]}";
String json3 = "{\"option\": {\"1\": \"myString1\"}}";
Field field1 = mapper.readValue(json1, Field.class);
Field field2 = mapper.readValue(json2, Field.class);
Field field3 = mapper.readValue(json3, Field.class);
英文:
Because you want to encapsulate a polymorphic value into the Object
field of your Field
class the task can be solved with a custom StdDeserializer
deserializer distinguishing the three cases you indicated (string, array, and map). So if you take a simplified version of your classes like below:
@Data
public class Field {
@JsonDeserialize(using = OptionDeserializer.class)
private Object option;
}
@Data
public class DetFieldOption {
private String value;
private String label;
}
You can define a custom stdDeserializer
that can return a different object in the three cases you indicated checking if the JsonNode
option is a string, an array, or an object:
public class OptionDeserializer extends StdDeserializer<Object> {
public OptionDeserializer() {
super(Object.class);
}
@Override
public Object deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
ObjectCodec codec = jp.getCodec();
JsonNode node = codec.readTree(jp);
//jsonnode is an array so it will be converted to DetFieldOption[]
if (node.isArray()) {
return codec.treeToValue(node, DetFieldOption[].class);
}
//jsonnode is a pojo so to convert it to a Map<String, String>
//it is necessary to define a new TypeReference
//no key as Integer because in a json keys are always string
//but you can convert them to integer if you need
if (node.isObject()) {
TypeReference typeRef = new TypeReference<Map<String, String>>(){};
Map<String, String> map = codec.readValue(codec.treeAsTokens(node),
dc.getTypeFactory().constructType(typeRef));
return map;
}
//default case node is a string
return node.asText();
}
}
Then you can convert json strings like the following below to Field
objects:
String json3 = """
{"option": "myString"}
""";
String json2 = """
{"option": [{"value": "myValue"}]}
""";
String json3 = """
{"option": {"1": "myString1"}}
""";
Field field1 = mapper.readValue(json1, Field.class);
Field field2 = mapper.readValue(json2, Field.class);
Field field3 = mapper.readValue(json3, Field.class);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论