序列化和反序列化(redis为例)
Jackson @JsonTypeInfo 详解笔记
一、@JsonTypeInfo 作用
场景:JSON 反序列化时 Jackson 不知道把一段
{}
转成哪个具体类。做法:在 JSON 里额外写一个类型标识,Jackson 根据这个标识找到对应类完成还原。
注解位置:字段或类上均可;只有出现多态的那一层才需要。
二、@JsonTypeInfo 全属性速查
属性 | 取值 | 说明 | 举例 |
---|---|---|---|
use | Id.CLASS | 用全限定类名作为类型 id | "@clazz": "com.xiaoyu.Child" |
Id.MINIMAL_CLASS | 去掉公共包前缀 | "@clazz": ".Child" (要求同包) | |
Id.NAME | 自定义名字(需配合 @JsonSubTypes ) | "@type": "child" | |
Id.CUSTOM | 完全自定义解析器 | 实现 TypeIdResolver | |
Id.NONE | 不额外写类型 | 很少用 | |
include | As.PROPERTY (默认) | 新增一个同级属性 | {"@clazz":"xxx","name":"Tom"} |
As.WRAPPER_OBJECT | 用类名做外层 key | {"Child":{"name":"Tom"}} | |
As.WRAPPER_ARRAY | 整个对象变成 [id,{}] | ["com.xxx.Child",{"name":"Tom"}] | |
As.EXTERNAL_PROPERTY | 属性写在父级节点 | 仅集合/Map 场景 | |
As.EXISTING_PROPERTY | 只复用已有字段 | 字段值=类型 id | |
property | 任意字符串 | 当 include=PROPERTY 时属性名 | 默认 "@type" ,可改 "@clazz" |
visible | true/false | 反序列化后是否保留该属性值到 Java 字段 | 默认 false |
defaultImpl | Class<?> | 找不到类型 id 时回退的类 | defaultImpl = UnknownModel.class |
@JsonTypeInfo
├─ use → 用什么当类型 id(CLASS/NAME/MINIMAL_CLASS...)
├─ include → id 放哪(PROPERTY / WRAPPER_OBJECT / EXTERNAL_PROPERTY...)
├─ property → 属性名(默认 "@type")
├─ visible → 反序列化后是否保留 id 值
└─ defaultImpl→ 找不到 id 时的兜底类
三、代码示例(覆盖全部常用组合)
1. CLASS + PROPERTY(最常用,零配置)
@Data
public class RedisDataDTO<T> {@JsonTypeInfo(use = Id.CLASS, include = As.PROPERTY, property = "@clazz")private T data;
}
JSON 结果
{"@clazz": "java.util.ArrayList","data": [1, 2, 3]
}
Redis序列化和反序列化 @ 配置 详解笔记
一、全局 activateDefaultTyping
会不会污染?
@Beanpublic ObjectMapper objectMapper() {ObjectMapper om = new ObjectMapper().setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY).registerModule(new JavaTimeModule()).disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)// 关键:忽略 POJO 中不认识的字段.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);;/*** 不加这种全局改变的,会导致字段污染,直接修改DTO类,让他在写入redis前,加上类的信息即可。* 打开后,所有没有被 @JsonTypeInfo 显式覆盖的 非 final 类型(List、Map、你的自定义 DTO …)* 都会在 JSON 里多出一个 @class 字段。*/
// .activateDefaultTyping(
// LaissezFaireSubTypeValidator.instance,
// ObjectMapper.DefaultTyping.NON_FINAL,
// JsonTypeInfo.As.PROPERTY);// 兼容 LocalDate 空格格式om.configOverride(LocalDate.class).setFormat(JsonFormat.Value.forPattern("yyyy-MM-dd"));// 兼容 LocalDateTime 空格格式om.configOverride(LocalDateTime.class).setFormat(JsonFormat.Value.forPattern("yyyy-MM-dd HH:mm:ss"));return om;}
结论
全局
activateDefaultTyping
会污染
所有非 final 类型(List、Map、自定义 DTO)都会被加上"@class"
字段。污染后果
JSON 体积膨胀;
类路径一旦重构,老数据直接反序列化失败;
与前端/MQ 共用的 DTO 也会带
@class
,属于无差别污染。
→ 企业落地 → 关闭全局开关,只在必要字段手动加 @JsonTypeInfo
,见第四部分代码。
二、@JsonTypeInfo 到底加在哪一层?
@JsonTypeInfo需不需要加
有多态就要加,运行期能100%确定类型,就不需要加,详细见本文最后的“TypeReference 与 @JsonTypeInfo 是否必须搭配” 章节
@Data
public class RedisDataDTO<T> {@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, // 用全限定类名include = JsonTypeInfo.As.PROPERTY, // 以属性形式出现property = "@clazz") // 属性名private List<T> data;private LocalDateTime expireTime;
}
规则
只加在“会出现多态”的字段上;
不需要把 T 里面所有类都贴一遍;
本例中
RedisDataDTO.data
字段是T
,运行时可能是List/Map/Child
等未知类型,只在这一层加即可;反序列化时 Jackson 发现:
字段上有
@JsonTypeInfo
;JSON 里有
"@clazz": "com.xxx.Child"
;用该字符串反射加载类并还原对象;
T 里面的普通字段/类无需任何注解,除非它们自身也可能多态(如
List<? extends BaseInterface>
),那就在对应子字段再加一次。
三、TypeReference 干嘛的?
3.1、技术背景
Jackson
convertValue
签名:<T> T convertValue(Object fromValue, JavaType/JavaTypeReference<T> toValueType)
Java 泛型运行时擦除,直接写
mapper.convertValue(redisValue, RedisDataDTO.class)
只能得到RedisDataDTO<Object>
,里面List<Map<String, Child>>
会退化成LinkedHashMap
,强转即 ClassCastException;TypeReference
利用匿名内部类保存完整泛型签名,让 Jackson 运行期仍知道:
“我要的是RedisDataDTO<List<Map<String, Child>>>
”
→ 一层层把LinkedHashMap
转成真正的Child
对象。
3.2、其它方法 JavaType , 二者对比如下:
// 1. 同一数据源
Object redisValue = redisTemplate.opsForValue().get("order:1");// 2. JavaType 写法
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(RedisDataDTO.class, OrderDTO.class);
RedisDataDTO<OrderDTO> dto1 = objectMapper.convertValue(redisValue, javaType);// 3. TypeReference 写法
RedisDataDTO<OrderDTO> dto2 = objectMapper.convertValue(redisValue,new TypeReference<RedisDataDTO<OrderDTO>>() {});// 4. 验证
System.out.println(dto1.getData().getClass()); // class OrderDTO
System.out.println(dto2.getData().getClass()); // class OrderDTO
System.out.println(dto1.equals(dto2)); // true
3.3、二者 嵌套 3 层写法对比
需求:List<Map<String, Set<OrderDetail>>>
TypeReference(写死)
TypeReference<RedisDataDTO<List<Map<String, Set<OrderDetail>>>>> ref =new TypeReference<>() {};
var data = objectMapper.convertValue(redisValue, ref);
JavaType(可动态拼)
JavaType inner = objectMapper.constructType(OrderDetail.class);
JavaType setType = objectMapper.getTypeFactory().constructParametricType(Set.class, inner);
JavaType mapType = objectMapper.getTypeFactory().constructParametricType(Map.class, objectMapper.constructType(String.class), setType);
JavaType listType = objectMapper.getTypeFactory().constructParametricType(List.class, mapType);
JavaType dtoType = objectMapper.getTypeFactory().constructParametricType(RedisDataDTO.class, listType);var data = objectMapper.convertValue(redisValue, dtoType);
3.4、一句话总结
功能等价——都能完整还原泛型,不会退化成
LinkedHashMap
;选型看场景——
– 编译期就确定 →TypeReference
简洁;
– 运行期动态拼 →JavaType
灵活;性能差异可忽略,可读性/可维护性才是选型重点。
3.5 、结论
没有 TypeReference 或者JavaType,嵌套泛型必然 ClassCastException。
四、零污染改造代码(部分代码参考)
1. 只污染 data 字段
@Data
public class RedisDataDTO<T> implements Serializable {private static final long serialVersionUID = 1L;/* 只在这一层打开多态,范围最小 */@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = As.PROPERTY, property = "@clazz")private T data;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime expireTime;
}
2. ObjectMapper 去掉全局开关
@Bean
public ObjectMapper objectMapper() {return new ObjectMapper().registerModule(new JavaTimeModule()).disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);// ****** 不再调用 activateDefaultTyping ******
}
3. RedisTemplate、RedisUtil 保持原样
JSON 里只有 data 字段多一个
@clazz
;其它任何 DTO/VO 零污染。
五、快速验证
// 写
List<Map<String, Set<OrderDetail>>> data = buildData();
redisUtil.set("k1", data, 60);// 读
var back = redisUtil.get("k1",new TypeReference<RedisDataDTO<List<Map<String, Set<OrderDetail>>>>>() {});System.out.println(back.getData().get(0).get("key").get(0).getClass());
// class OrderDetail (不是 LinkedHashMap)
六、速记一句话
全局
activateDefaultTyping
别用 → 字段级@JsonTypeInfo
足够;@JsonTypeInfo
只贴在最外层“会多态”的字段”,子类/子字段无需重复贴;TypeReference
必须给,否则运行期泛型擦除 → LinkedHashMap 强转爆炸;改造后 只有必要字段多一个
@clazz
,零污染、可升级、可兼容。
TypeReference 与 @JsonTypeInfo 是否必须搭配?
一、两者职责划分
功能 | 技术 | 是否必须 |
---|---|---|
把泛型层数对齐 防止 List<Map<String,Child>> 退化成 LinkedHashMap | TypeReference 或 JavaType | ✅ (嵌套时必须) |
把多态对象还原成正确子类 (Dog vs Cat / 实现类 vs 接口) | @JsonTypeInfo + @clazz /@type | ✅ (出现子类/实现类时必须) |
→ “TypeReference 管层数,@JsonTypeInfo 管多态”
→ 二者无强制绑定关系,按场景按需选用。
二、不需要 @JsonTypeInfo 的场景(无多态)
代码示例
@Data
public class RedisDataDTO<T> {private T data; // 无 @JsonTypeInfo
}// 存
List<OrderDTO> list = List.of(new OrderDTO(1));
redisUtil.set("k1", list, 60);// 取
var back = objectMapper.convertValue(redisValue,new TypeReference<RedisDataDTO<List<OrderDTO>>>() {});
实际 JSON
{"data": [{ "id": 1, "name": "订单" }],"expireTime": "2025-06-20 15:30:00"
}
运行期 100% 确定为 OrderDTO,无子类; /** 关键 */
结论:
TypeReference
已足够,不需要@JsonTypeInfo
。
三、必须加 @JsonTypeInfo 的场景(有多态)
代码示例
@Data
public class RedisDataDTO<T> {@JsonTypeInfo(use = Id.CLASS, property = "@clazz") // 必须加private T data;
}// 存:List 里放 Dog 和 Cat 两种子类
List<Animal> animals = List.of(new Dog(), new Cat());
redisUtil.set("k2", animals, 60);// 取
var back = objectMapper.convertValue(redisValue,new TypeReference<RedisDataDTO<List<Animal>>>() {});
实际 JSON
{"data": [{ "@clazz": "com.xiaoyu.Dog", "bone": 1 },{ "@clazz": "com.xiaoyu.Cat", "fish": 2 }],"expireTime": "2025-06-20 15:30:00"
}
运行期可能是
Dog
/Cat
任意子类;没有
@clazz
Jackson 无法还原子类;结论:
TypeReference
只管泛型层数,子类还原必须@JsonTypeInfo
。
四、一句话速记
“TypeReference 管层数,@JsonTypeInfo 管多态;只要存子类/实现类,就必须加
@JsonTypeInfo
,与用不用TypeReference
无关。”
五、扩充
“JSON +
@JsonTypeInfo
+ TypeReference/JavaType”这套组合拳在 Redis、RabbitMQ、Elasticsearch 里全部通用;
差异只在外围配置(MQ 白名单、ES 不写@clazz
),核心规则不变。