Spring Boot 4/Hibernate 7:JsonNode 写入 jsonb 失败
可以继续使用 tools.jackson.databind.JsonNode。这条异常表明 Jackson 2 正在尝试反序列化 Jackson 3 的树节点类型。先让 Hibernate 的 JSON 映射器与字段类型使用同一代 Jackson,无须直接改成 byte[]。
问题没有提供 Hibernate 的具体小版本和完整调用栈,需要根据实际依赖选择配置方案。
为什么异常来自 com.fasterxml.jackson?
异常中的两个包名含义不同:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:抛出异常的是 Jackson 2。Cannot construct instance of tools.jackson.databind.JsonNode:要创建的目标仍然是 Jackson 3 的JsonNode。
目标类型并没有变成 com.fasterxml.jackson.databind.JsonNode。这里是 Jackson 2 的反序列化机制在处理 Jackson 3 类型,因此无法使用该类型对应的树模型反序列化支持。
Spring Boot 4 默认采用 Jackson 3,相关包名从 com.fasterxml.jackson 迁移到了 tools.jackson,但依赖 Jackson 2 的其他库仍可能同时存在。参见 Spring Boot 4 迁移指南。
Hibernate 通过 FormatMapper 转换 JSON 列。例如,Hibernate 7.1 的 JacksonJsonFormatMapper 使用 com.fasterxml.jackson.databind.ObjectMapper。不能认定 Spring Boot 为 HTTP JSON 配置的 mapper 也被 Hibernate 使用。参见 Hibernate 7.1 映射器源码。
@JdbcTypeCode(SqlTypes.JSON) 指定 JSON 类型映射,columnDefinition = "jsonb" 声明数据库列类型。这两项配置不会自动解决 Jackson 2 与 Jackson 3 的类型兼容问题。
先确认实际依赖和调用路径
如果使用 Maven,可以检查依赖树:
./mvnw dependency:tree
重点查看 hibernate-core 的具体版本,以及是否同时存在以下依赖:
com.fasterxml.jackson.core:jackson-databind
tools.jackson.core:jackson-databind
同时检查完整堆栈是否经过 Hibernate 的 JacksonJsonFormatMapper。异常包名可以确认 Jackson 2 参与了处理,但仅凭当前异常片段,还不能确定 mapper 是在哪里创建的。
保存过程中,深复制、状态快照或回读也可能触发反序列化。因此,不能只根据“保存时报错”就认定问题发生在 SQL 写入阶段。
如何让 Hibernate 使用 Jackson 3?
实际版本提供内置映射器时
先确认所用 Hibernate 版本包含以下类:
org.hibernate.type.format.jackson.Jackson3JsonFormatMapper
例如,Hibernate 7.4 的 API 文档中列出了该类。如果实际依赖提供它,可以在 application.properties 中显式指定:
spring.jpa.properties.hibernate.type.json_format_mapper=org.hibernate.type.format.jackson.Jackson3JsonFormatMapper
原来的实体字段可以保留:
@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
private tools.jackson.databind.JsonNode data;
不要仅凭“Hibernate 7”就复制该配置。如果当前依赖没有这个类,可以采用适配方案,或在确认 Spring Boot 依赖兼容性后升级。
实际版本没有内置映射器时
可以通过 FormatMapper SPI 接入 Jackson 3。Hibernate 支持使用 hibernate.type.json_format_mapper 指定映射器实例、类或完整类名,参见 Hibernate 配置说明。
下面的示例沿用 Hibernate 7.1 的 AbstractJsonFormatMapper,适用于当前 JsonNode 字段:
package com.example.persistence;
import java.lang.reflect.Type;
import org.hibernate.type.format.AbstractJsonFormatMapper;
import tools.jackson.databind.json.JsonMapper;
public final class Jackson3FormatMapper extends AbstractJsonFormatMapper {
private final JsonMapper mapper = JsonMapper.builder().build();
@Override
protected <T> T fromString(CharSequence json, Type type) {
return mapper.readValue(
json.toString(),
mapper.constructType(type)
);
}
@Override
protected <T> String toString(T value, Type type) {
return mapper.writerFor(mapper.constructType(type))
.writeValueAsString(value);
}
}
将配置改为自定义类的完整名称:
spring.jpa.properties.hibernate.type.json_format_mapper=com.example.persistence.Jackson3FormatMapper
该基类保留了 Hibernate 对 String 等类型的处理方式,参见 AbstractJsonFormatMapper 源码。
示例自行创建了 JsonMapper。如果其他 JSON 字段依赖 Spring Boot 配置的模块或自定义序列化规则,需要将配置好的 mapper 显式传给适配器,并通过 Hibernate 配置注册适配器实例。只声明一个 Spring Bean,不能保证 Hibernate 自动使用它。
能否暂时使用 Jackson 2 的 JsonNode?
可以,前提是持久化层继续使用 Jackson 2 的映射器。此时字段应同步改为:
import com.fasterxml.jackson.databind.JsonNode;
创建和解析该字段值时,也要使用 Jackson 2 的 ObjectMapper。两个包下的 JsonNode 是不同类型,不能直接赋值或强制转换。
如果接口层已经使用 Jackson 3,需要在边界显式转换。这个方案适合作为迁移过渡,但会增加两套树模型之间的转换工作。
是否应该改成 String 或 byte[]?
是否改类型取决于业务如何使用 JSON:
JsonNode:适合动态读取和修改 JSON 树,修正映射器后可以继续使用。Map<String, Object>或业务 POJO:适合对象结构较明确的场景,仍需正确的 JSON 映射器。String:适合保存完整 JSON 文本。使用 Hibernate 的标准 JSON 映射时,应保留@JdbcTypeCode(SqlTypes.JSON),并传入合法 JSON 文本,避免再次序列化而产生多余引号。byte[]:通常用于二进制数据。直接替换字段类型不能解决 JSON 树模型的兼容问题,也不等同于配置 PostgreSQLjsonb。
完成调整后,应验证保存、flush、清空持久化上下文后重新查询,以及修改 JSON 后再次保存。还要检查嵌套对象、数组、JSON null 和 SQL NULL 是否符合预期,避免只确认首次写入成功。
备注:内容仅供参考。
