将Typescript的动态类型映射到Java对象的子类中

huangapple go评论63阅读模式
英文:

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: &#39;center&#39; | &#39;left&#39;; // 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; // &#39;true&#39; | &#39;plugin:...&#39;  
  invisible: &#39;true&#39; | &#39;false&#39;; // never used  
  optional: string; // just in 3 fields, never used 
  option:
    | string // &#39;plugin:...&#39;    
    | { value: string; label: string; disable?: string; labelEng?: string, valueOfAnswerInInterestType?: string }[] //FieldOption
    | { 1?: string; 2?: string; 3?: string; 4?: string; 5?: string; 6?: string };  //Map&lt;Integer,String&gt;
}

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; // &#39;plugin:...&#39; | [{ value: 1, label: &#39;my value&#39; }, ...] | { &quot;1&quot;: &quot;Option one ...&quot;}
}

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

Without yet working on the case of Map&lt;Integer,String&gt;, 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.&lt;init&gt;(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 Objectfield 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&lt;Object&gt; {

    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&lt;String, String&gt;
        //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&lt;Map&lt;String, String&gt;&gt;(){};
            Map&lt;String, String&gt; 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 = &quot;&quot;&quot;
                    {&quot;option&quot;: &quot;myString&quot;}
              &quot;&quot;&quot;;

String json2 = &quot;&quot;&quot;
                    {&quot;option&quot;: [{&quot;value&quot;: &quot;myValue&quot;}]}
               &quot;&quot;&quot;;
String json3 = &quot;&quot;&quot;
                    {&quot;option&quot;: {&quot;1&quot;: &quot;myString1&quot;}}
               &quot;&quot;&quot;;
Field field1 = mapper.readValue(json1, Field.class);
Field field2 = mapper.readValue(json2, Field.class);
Field field3 = mapper.readValue(json3, Field.class);

huangapple
  • 本文由 发表于 2023年3月3日 19:36:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/75626593.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定