升级 Jackson 后 Properties 子类反序列化失败,如何排查?
结论
这段异常表明,properties 属性要求的类型是 NodeProperties,反序列化时传入的却是普通的 java.util.Properties。父类实例不能赋给子类类型的属性,所以发生了 argument type mismatch。
现有信息不足以认定这是 Jackson 3.x 的预期行为,也无法据此推荐能修复问题的配置开关。应先核对实际运行的 Jackson 版本,再检查为什么没有创建 NodeProperties。如果需要先恢复读取能力,可以将 JSON 中的属性读为普通 Map,再显式构造 NodeProperties。
先核对实际运行的 Jackson 版本
问题中的包名和所述版本有一处需要核对:Jackson 3 的 databind 包名已迁移到 tools.jackson.databind,但异常来自 com.fasterxml.jackson.databind.JsonMappingException。jackson-annotations 是例外,保留了原包名。Jackson 官方迁移指南
这表明出错的调用路径涉及 Jackson 2 的 databind 类。仅凭这条异常,不能确定整个应用的依赖情况;两个主版本并存也不一定意味着有问题。
在调用实际发生错误的位置,检查使用的 mapper 实例:
System.out.println(mapper.getClass().getName());
System.out.println(mapper.version());
System.out.println(mapper.getClass().getProtectionDomain().getCodeSource());
再检查依赖树。例如,Maven 项目可以执行:
mvn dependency:tree
重点核对 jackson-databind、jackson-core、扩展模块,以及框架实际注入的 mapper。如果两个主版本并存,确认每条调用路径使用的是哪个版本。
定位子类实例在哪里丢失
从异常中的两个类型入手:
expected type: my.application.entities.NodeProperties
actual type: java.util.Properties
使用发生错误的同一个 mapper,将同一段最小属性 JSON 分别读取为 Properties.class 和 NodeProperties.class。这里应传入 properties 对应的 JSON 对象,而不是整个 RootNode 文档。
- 如果读取普通
Properties成功、读取NodeProperties失败,重点检查子类构造方式、注册的反序列化器、转换器和实例化逻辑。 - 如果单独读取
NodeProperties成功,但读取RootNode失败,重点检查properties的字段、setter、构造参数及其注解。 - 如果有自定义处理逻辑,确认它返回的是
NodeProperties,而不是new Properties()。
还要确认 NodeProperties 是否提供了供映射器使用的构造方法。Java 不会继承父类的构造方法。不过,这条异常显示实际生成的是父类对象,不能据此断定缺少无参构造方法就是根因。
完整类定义、具体依赖版本和最小 JSON 均未提供,目前还无法将原因锁定到某处配置或某个版本缺陷。
兜底处理:先读取 Map,再构造子类
如果旧 JSON 中的 properties 是普通的字符串键值对象,可以在加载边界增加一个 DTO,避免直接依赖 Properties 子类的自动实例化。
下面的示例采用 Jackson 3 的包名,假设输入结构为:
{
"properties": {
"timeout": "30"
}
}
import java.util.Map;
import java.util.Properties;
import tools.jackson.databind.json.JsonMapper;
public class ConfigLoader {
public static class ConfigDto {
public Map<String, String> properties;
}
public static class NodeProperties extends Properties {
public NodeProperties() {
super();
}
public NodeProperties(Properties parent) {
super(parent);
}
}
public static NodeProperties readProperties(
JsonMapper mapper, String json, Properties parent) {
ConfigDto dto = mapper.readValue(json, ConfigDto.class);
if (dto == null || dto.properties == null) {
throw new IllegalArgumentException("properties is required");
}
NodeProperties result = new NodeProperties(parent);
dto.properties.forEach((key, value) -> {
if (key == null || value == null) {
throw new IllegalArgumentException(
"Property keys and values must not be null");
}
result.setProperty(key, value);
});
return result;
}
}
这个示例只负责加载和转换。实际接入时,应保留原有 NodeProperties 的辅助方法,并将返回对象交给 RootNode。如果配置包含其他字段或子节点,需要按实际结构在 DTO 中补齐。
如果继续使用直接绑定,也可以为 NodeProperties 编写专用的反序列化器,并确保它返回正确的子类实例。注册方式和 API 应与确认的 Jackson 主版本一致。
单独恢复父级默认值关系
本地键值对读取成功后,还需确认父级关系是否已经恢复。如果 JSON 没有保存父级关系,加载后也没有重新连接对象,就无法仅凭本地属性重建整棵配置树。
可以先恢复父节点,再通过 new NodeProperties(parent) 创建子节点。使用 getProperty(key) 读取继承值:如果本地不存在相应的字符串属性,它会继续搜索 defaults 链。普通的 get(key) 不提供这种默认值回退语义。Java Properties API
保存配置时,应区分本地显式值和从父级继承的有效值。如果将继承值全部写入子节点,可能改变后续的覆盖行为。
备注:内容仅供参考。
