Fork me on GitHub
15 July 2022

开发通用接口时,参数序列化/反序列化是一个比较棘手的问题,参数类型不好确定。简单直接办法是添加一个容器类型的对象,包裹真实参数,通过特殊接口获取真实类型后再进行转换。这样增加了开发复杂度和代码量,那有没有更简单的方法?

幸好 Jackson 提供 DefaultTyping 允许用户自定义序列化/反序列化对象类型,参考 ObjectMapper#setDefaultTyping,另外 JsonTypeInfo 注解也有类似功能。原理是序列化时读取对象类型(DefaultTyping),并把类型写入生成的 JSON 中,反序列化时读取类型字段获取类型并反序列化为该类型实例。可能带来安全问题,谨慎使用。

不过 ObjectMapper#setDefaultTyping 调用比较麻烦,推荐使用 ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)。使用示例:

// 提供 `ioDataType` 属性
public interface DataType {

    default String getIoDataType() {
        return getClass().getName();
    }

}

public record User(String username, String password) implements DataType {

}

// 激活 DefaultTyping
objectMapper.activateDefaultTypingAsProperty(
    objectMapper.getPolymorphicTypeValidator(), 
    ObjectMapper.DefaultTyping.NON_FINAL, 
    "ioDataType"
);

// 序列化
objectMapper.writeValueAsString(new User("xxxx", "123"))

//序列化后的 JSON 已包含 ioDataType 字段,对象类型为 com.example.demo.record.User
// {"username":"xxxx","password":"123","ioDataType":"com.example.demo.record.User"}

注意使用 Record 类型的数据时未把类型加入到结果中,原因是设置为 DefaultTyping.NO_FINAL ,而 Record 对象的字段都是 FINAL 修饰,所以无法生效。参考 com.fasterxml.jackson.databind.ObjectMapper.DefaultTypeResolverBuilder#useForType,改为使用 EVERYTHING,注意可能带来安全隐患。更安全的的方案是各个数据实体类都实现特定接口,提供 getXxx 以便提供类型属性。