使用JsonTypeInfo进行序列化/反序列化

3

我的目标是使用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;

除了这个类,我还有一个 DB 表格,其中存储了 AnimalRecord(AnimalRecord=行) 的记录。 AnimalMetadata 是基于该类的 source 不同的 JSON 字符串。每个 source 都有自己的 metadata 和类定义。在此示例中,当 source 为“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;
        }

    }

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

问题:我是否必须将“source”字段添加到元数据本身,或者还有其他更好的方法可以做到这一点,我可能错过了吗?

更新示例: DB行的示例: id | source | metadata 1 | catSource | {"catName": "Mewy"} 2 | dogSource | {"dogName": "Barky"}

当我从DB中读取行时,我想使用source字段将metadata反序列化为正确的类-String --> CatMetadata

2个回答

2
杰克逊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>

1

@JsonTypeInfo注释的property属性标记定义实体子类的属性,include = JsonTypeInfo.As.EXTERNAL_PROPERTY表示此属性应包含在AnimalRecord类的属性中,而不是在metadata值内。仅当您将字符串解析为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));

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

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

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,好的回答! 但我不明白为什么你要将setSource设置为具有类型和猫名的catRecord,我认为source应该只是cat,而metadata应该包括内部类的所有元数据(如猫名)。能解释一下吗? - TheUnreal
抱歉,我不太确定我理解了,你可能是指“内部类”是子类吗?据我所知,您在数据库中有一个字符串字段,它可能包含不同类型的数据,因此您想通过多态地解析它(只指定根类 AnimalMetadata 并让 Jackson 找出要使用的实际子类)。catNamedogName 只是 AnimalMetadata 子类中可能不同的字段示例。它们也可能包含完全相同的字段集,但那样的话,为什么您需要不同的子类就不清楚了。希望我的回答有意义。 - Forketyfork
@TheUnreal 我想我看到了混淆的“源头” - 我以为你的“源”字段包含你想要解析的JSON,并且你想将原始字符串保留在那里作为你的数据模型的一部分。我已经更新了示例。 - Forketyfork
仍然不是,你在评论中说的是对的 - 但我预计可能不同的字段将在父类的“元数据”中设置,而不是在“类型”字段中,该字段应该是一个简单的字符串 - “cat”或“dog”。我在我的问题中添加了一个示例。编辑:我看到了你的评论,谢谢,让我检查一下。 - TheUnreal
所以我理解你不想在元数据本身中添加“source”字段。据我所知,根据某些外部字段解析JSON是不可能的。您也可以使用switch(record.source),并基于此选择要解析的类。这与在注释中指定相同信息并没有太大区别。 - Forketyfork
我已经更新了答案,并提供了一个可行的示例。 - Forketyfork

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接