【问题标题】:Serialize/Deserialize using JsonTypeInfo使用 JsonTypeInfo 进行序列化/反序列化
【发布时间】:2021-05-08 13:32:09
【问题描述】:

我的目标是使用 Jackson 将 JSON 字符串字段转换为正确的类。

我有以下课程:

public class AnimalRecord {

    private String id;
    private String source;
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "source", include = JsonTypeInfo.As.EXTERNAL_PROPERTY)
    @JsonSubTypes(value = {
            @JsonSubTypes.Type(value = CatProbeMetadata.class, name 
 = "catProbeMetadata"),
    @JsonSubTypes.Type(value = DogProbeMetadata.class, name = "dogProbeMetadata"),
            })
   private AnimalMetadata metadata;

除了这个类,我还有一个数据库表,用于存储 AnimalRecord 的记录(AnimalRecord = 行)。 AnimalMetadata 是基于此类的 source 的不同 JSON 字符串。每个来源都有自己的metadata 和类定义。在这个例子中,当源为“cat”时,CatProbeMetadata 类将作为对字符串进行反序列化时的输出。

问题是我不确定从数据库中读取行时该怎么做。我有以下方法:

private class ActiveProbeWrapper implements RowMapper<ActiveProbeRecord> {

        @Override
        public ActiveProbeRecord mapRow(ResultSet rs, int rowNum) throws SQLException {
            String id= rs.getString("id");
            String source= rs.getString("source");
            Animalmetadata metadata = // NOT SURE WHAT TO DO HERE;
            ActiveProbeRecord record = new ActiveProbeRecord(deviceId,segment, source, metadata);
            return record;
        }

    }

我需要将数据库中的字符串转换为正确的类实例,但我的元数据字符串不会包含源(因为它在元数据 JSON 之外)。

问题:我是否必须将“源”字段添加到元数据本身,或者有没有更好的方法可以做到这一点?

更新示例: 数据库行示例: 编号 |来源 |元数据 1 |猫源 | {“猫名”:“喵喵”} 2 |狗源 | {"dogName": "Barky"}

当我从数据库中读取行时,我想使用 source 字段将 metadata 反序列化到正确的类 - 字符串 --> CatMetadata

【问题讨论】:

    标签: java spring-boot jackson


    【解决方案1】:

    @JsonTypeInfo 注解的property 属性标记了定义实体子类的属性,include = JsonTypeInfo.As.EXTERNAL_PROPERTY 表示该属性不应包含在metadata 值内,而应包含在上层,作为AnimalRecord 类的属性。这仅在您将字符串解析为 AnimalRecord 类时才有效。

    此属性应包含值 catProbeMetadata 用于猫和 dogProbeMetadata 用于狗,否则 Jackson 将不知道如何解析您的 source 字段的内容。该属性也可能包含在source 字符串本身中,但您必须使用include = JsonTypeInfo.As.PROPERTY

    方法 1 - 类型在元数据中

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
    @JsonSubTypes({
            @JsonSubTypes.Type(value = CatProbeMetadata.class, name = "catProbeMetadata"),
            @JsonSubTypes.Type(value = DogProbeMetadata.class, name = "dogProbeMetadata"),
    })
    class AnimalMetadata {
    
        private String type;
    }
    
    class CatProbeMetadata extends AnimalMetadata {
    
        private String catName;
    }
    
    class DogProbeMetadata extends AnimalMetadata {
    
        private String dogName;
    }
    
    class AnimalRecord {
    
        private AnimalMetadata metadata;
    }
    
    

    那么你可以这样解析:

    ObjectMapper mapper = new ObjectMapper();
    
    AnimalRecord catRecord = new AnimalRecord();
    catRecord.setMetadata(mapper.readValue("{\"type\":\"catProbeMetadata\",\"catName\": \"Paws\"}", AnimalMetadata.class));
    
    AnimalRecord dogRecord = new AnimalRecord();
    dogRecord.setMetadata(mapper.readValue("{\"type\":\"dogProbeMetadata\",\"dogName\": \"Fido\"}", AnimalMetadata.class));
    

    方法 2 - 类型在元数据之外

    只需根据类型手动选择类。您不需要任何注释:

    class AnimalMetadata {
    }
    
    class CatProbeMetadata extends AnimalMetadata {
        private String catName;
    }
    
    class DogProbeMetadata extends AnimalMetadata {
        private String dogName;
    }
    
    class AnimalRecord {
    
        private String type;
    
        private AnimalMetadata metadata;
    }
    

    那你就可以这样解析了。将选择逻辑放在单独的方法中与将其放入注释中的结果完全相同 - 如果您想添加新的子类,只需更新一段不同的代码:

    public Class<? extends AnimalMetadata> getMetadataClass(AnimalRecord record) {
        switch (record.getType()) {
            case "cat":
                return CatProbeMetadata.class;
            case "dog":
                return DogProbeMetadata.class;
            default:
                throw new UnsupportedOperationException();
        }
    }
    
    public void parse() {
        ObjectMapper mapper = new ObjectMapper();
    
        AnimalRecord catRecord = new AnimalRecord();
        catRecord.setType("cat");
        catRecord.setMetadata(mapper.readValue("{\"catName\": \"Paws\"}", getMetadataClass(catRecord)));
    
        AnimalRecord dogRecord = new AnimalRecord();
        dogRecord.setType("dog");
        dogRecord.setMetadata(mapper.readValue("{\"dogName\": \"Fido\"}", getMetadataClass(dogRecord)));
    }
    
    

    【讨论】:

    • 谢谢@Sergei,很好的回答!但我不明白为什么你 setSourcecatRecord 带有类型和猫名,我认为 source 应该只是 cat 并且元数据`应该包括内部的所有元数据(如猫名)班级。你能解释一下吗?
    • 对不起,我不确定我是否遵循,可能“内部类”是指子类?据我了解,您在数据库中有一个可能包含不同类型数据的字符串字段,因此您想多态地解析它(仅指定根类 AnimalMetadata 并让杰克逊找出要使用的实际子类)。 catNamedogName 只是 AnimalMetadata 子类中可能不同的字段的示例。它们也可能包含完全相同的字段集,但是不清楚为什么需要为它们设置不同的子类。希望我的回答有意义。
    • @TheUnreal 我想我看到了混乱的“来源” - 我认为您的“来源”字段包含您要解析的 JSON,并且您希望将原始字符串保留在那里作为您的一部分数据模型。我已经更新了示例。
    • 还是不行,你在评论中说的是对的——但我希望可能不同的字段将设置在父类的metadata 中,而不是type 字段中,这应该是一个简单的字符串 - catdog。我在我的问题中添加了一个示例。编辑:我看到你了,谢谢,让我检查一下
    • 所以我知道您不想在元数据本身中添加 source 字段。据我所知,根据某些外部字段解析 JSON 是不可能的。您也可以只做一个switch(record.source) 并选择要基于它解析的类。这与在注释中指定相同的信息并没有真正的区别。
    【解决方案2】:

    Jackson 2.12 引入了一个 new feature for type deduction

    @JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION)
    @JsonSubTypes({
            @JsonSubTypes.Type(DogMetadata.class),
            @JsonSubTypes.Type(CatMetadata.class) })
    public abstract class AnimalMetadata {
    }
    

    因此:

    AnimalMetadata metadata = om.readValue("{\"catName\": \"Paws\"}", AnimalMetadata.class);
    assertThat(metadata).isInstanceOf(CatMetadata.class);
    

    缺点是,如果 Jackson 无法仅根据属性名称确定要使用哪个子类型,它可能会中断。 使用此解决方案,可选的 json 字段(如缺少的 catName 属性)或过于相似的子类型可能会引发问题。 @Sergei 解决方案没有这些问题(另外,他的解决方案利用了 source 字段,这是您的要求)。

    附带说明,如果您正在使用 SpringBoot,升级 jackson 只需在 pom.xml 中添加此属性

            <jackson-bom.version>2.12.3</jackson-bom.version>
    

    【讨论】:

      猜你喜欢
      • 2021-12-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-02
      • 2012-05-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多