Jackson使用@JsonTypeInfo反序列化多态类型(根据标识解析为子类对象)
问题场景
jackson可以将多态类型JSON序列化. 但在反序列化时会因为找不到具体的类而失败.
举例:创建4个POJO类
@Data
public class AbstractTarget {
}
@Data
@EqualsAndHashCode(callSuper = true)
class HiveTarget extends AbstractTarget {
private String schema;
private String table;
private String column;
}
@Data
@EqualsAndHashCode(callSuper = true)
class HBaseTarget extends AbstractTarget{
private String namespace;
private String table;
private String columnFamily;
private String column;
}
@Data
class Statistics {
private List<AbstractTarget> targets;
}
测试方法
@Test
public void testDeserialize() throws JsonProcessingException {
Statistics statistics = new Statistics();
List<AbstractTarget> targets = new ArrayList<>();
statistics.setTargets(targets);
HiveTarget hiveTarget = new HiveTarget();
hiveTarget.setSchema("s1");
hiveTarget.setTable("t1");
hiveTarget.setColumn("c1");
targets.add(hiveTarget);
HBaseTarget hBaseTarget= new HBaseTarget();
hBaseTarget.setNamespace("ns2");
hBaseTarget.setTable("t2");
hBaseTarget.setColumnFamily("cf2");
hBaseTarget.setColumn("c2");
targets.add(hBaseTarget);
// 序列化
String statisticsStr = mapper.writeValueAsString(statistics);
System.out.println(statisticsStr);
// 反序列化
Statistics parsedStatistics = mapper.readValue(statisticsStr, Statistics.class);
System.out.println(parsedStatistics);
}
结果
{"targets":[{"schema":"s1","table":"t1","column":"c1"},{"namespace":"ns2","table":"t2","columnFamily":"cf2","column":"c2"}]}
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `fresh.json.AbstractTarget` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (String)"{"targets":[{"schema":"s1","table":"t1","column":"c1"},{"namespace":"ns2","table":"t2","columnFamily":"cf2","column":"c2"}]}"; line: 1, column: 13] (through reference chain: fresh.json.Statistics["targets"]->java.util.ArrayList[0])
...
因此若要正确的反序列化,需要指定具体子类的标识。
方式一.使用类名作为标识
如下:使用类名作为标识符,并将标识符作为属性序列化,属性名称指定为"@class"。
@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.PROPERTY,
property = "@class"
)
//等于@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS), 另外两个为类名作为标识的默认值
public class AbstractTarget {
}
序列化结果中会包含”@class“属性,反序列化时就会根据”@class“找到具体的类。
{"targets":[{"@class":"fresh.json.HiveTarget","schema":"s1","table":"t1","column":"c1"},{"@class":"fresh.json.HBaseTarget","namespace":"ns2","table":"t2","columnFamily":"cf2","column":"c2"}]}
实测发现当直接使用List序列化时(targets)会丢失"@class"属性,嵌套的列表和单独对象都没有这个问题。
...
System.out.println("serializing nested array-------");
System.out.println(mapper.writeValueAsString(statistics));
System.out.println("serializing object-------------");
System.out.println(mapper.writeValueAsString(hiveTarget));
System.out.println("serializing array--------------");
System.out.println(mapper.writeValueAsString(targets));
结果
serializing nested array-------
{"targets":[{"@class":"fresh.json.HiveTarget","type":null,"schema":"s1","table":"t1","column":"c1"},{"@class":"fresh.json.HBaseTarget","type":null,"namespace":"ns2","table":"t2","columnFamily":"cf2","column":"c2"}]}
serializing object-------------
{"@class":"fresh.json.HiveTarget","type":null,"schema":"s1","table":"t1","column":"c1"}
serializing array--------------
[{"type":null,"schema":"s1","table":"t1","column":"c1"},{"type":null,"namespace":"ns2","table":"t2","columnFamily":"cf2","column":"c2"}]
方式二.使用属性值作为标识
使用类名作为标识符的好处是配置方便,但是会在序列化中暴露类名,并且如果使用其他方式构造json串时可能需要手动设置”类名属性“。
另一种方式是使用属性值做为标识,配置较为繁琐,适合类中已经存在标识属性的情况。
如下:AbstractTarget存在type属性,并且在两个子类中设置了固定且不同的值,使用@JsonTypeInfo指定type属性作为”类标识“,同时需要使用@JsonSubTypes指定 具体类 和 type属性值 的关系。
@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = HiveTarget.class, name = HiveTarget.TYPE),
@JsonSubTypes.Type(value = HBaseTarget.class, name = HBaseTarget.TYPE)
})
public class AbstractTarget {
private String type;
}
@Data
@EqualsAndHashCode(callSuper = true)
class HiveTarget extends AbstractTarget {
private String schema;
private String table;
private String column;
static final String TYPE = "hive";
public HiveTarget(){
setType(TYPE);
}
}
@Data
@EqualsAndHashCode(callSuper = true)
class HBaseTarget extends AbstractTarget{
private String namespace;
private String table;
private String columnFamily;
private String column;
static final String TYPE = "hbase";
public HBaseTarget(){
setType(TYPE);
}
}
序列化结果就和普通的序列化一致,不会包含额外属性。
{"targets":[{"type":"hive","schema":"s1","table":"t1","column":"c1"},{"type":"hbase","namespace":"ns2","table":"t2","columnFamily":"cf2","column":"c2"}]}
Maven依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.2</version>
</dependency>
参考地址