Skip to content
LangChain4j 文档版本 1.22.0 源快照 c37b4099 更新于 2026-10-14

使用 Jackson 3

LangChain4j 在很多地方读写 JSON:它与 LLM 提供者交换的请求与响应、AI 服务解析的结构化输出、你持久化的聊天记忆等等。默认情况下,它使用 Jackson 2 来做这件事。关于 LangChain4j 如何使用 JSON、以及如何接入你自己的 mapper,参见 JSON。

如果你的应用在用 Jackson 3,你可以让 LangChain4j 改用它。

开启它

添加一个依赖:

xml
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-jackson3</artifactId>
    <version>1.22.0-beta32</version>
</dependency>

这就是全部了,对包括 langchain4j-agentic 在内的每个模块都如此。LangChain4j 通过 ServiceLoader 找到该模块,并把它的 JSON 路由给 Jackson 3。无需配置,也无需调用任何 API;如果你移除该依赖,一切又回到 Jackson 2。

该支持拆分为两个 artifact,而上面的 langchain4j-jackson3 会同时引入二者,所以除非你有理由不这么做,否则就加它:

Artifact覆盖范围依赖于
langchain4j-core-jackson3langchain4j-core 中的一切:提供者的请求与响应、结构化输出、聊天记忆序列化、智能体状态langchain4j-core
langchain4j-jackson3上述内容,外加 InMemoryEmbeddingStore 持久化,它位于 langchain4j 模块langchain4j-core-jackson3 与 langchain4j

仅当你不借助 langchain4j 模块、而是直接使用某个框架集成来构建模型(例如某个直接构建模型的框架集成)时,才单独添加 langchain4j-core-jackson3。将它与 langchain4j-jackson3 并加是不必要的,但也无害。

保持不变的部分

切换 JSON 库很容易在不经意间改变行为,所以本模块竭力避免这一点。Jackson 3 改了好几个默认值,而几乎每一个会改变 LangChain4j 读写内容的默认值都被设回了 Jackson 2 的行为。少数有意保留的差异列在有意为之的不同之下。

设置Jackson 3 默认本模块的做法
ALLOW_FINAL_FIELDS_AS_MUTATORS禁用启用
USE_GETTERS_AS_SETTERS禁用启用
SORT_PROPERTIES_ALPHABETICALLY启用禁用
FAIL_ON_TRAILING_TOKENS启用禁用
FAIL_ON_NULL_FOR_PRIMITIVES启用禁用
READ_ENUMS_USING_TO_STRING启用禁用:枚举按 name() 读取
WRITE_ENUMS_USING_TO_STRING启用禁用:枚举按 name() 写入
DETECT_PARAMETER_NAMES启用禁用,结构化输出与工具参数除外(见下文)
FAIL_ON_UNKNOWN_PROPERTIES禁用启用:在结构化输出、工具参数、聊天记忆、智能体状态与存储的嵌入存储中,目标类型没有的字段会导致失败;提供者响应仍会忽略未知字段
FAIL_ON_EMPTY_BEANS禁用启用:一个无可写入内容的对象——例如字段为 private 且没有 getter 的对象——会失败,而不是被发送为 {}

第一条最为要紧:若没有它,一个 final 集合字段会被留空而非填充,且不会有任何提示。

DETECT_PARAMETER_NAMES 只在你的代码用 -parameters 编译时才有影响,而 Spring Boot 与 Quarkus 项目通常都是如此。对于结构化输出与工具参数,会使用构造函数参数名,正如当 classpath 上有 jackson-module-parameter-names 时 Jackson 2 编解码器所做的那样(在 Spring Boot 与 Quarkus 中它是在的):一个只有带参构造函数的类,例如一个 Lombok @AllArgsConstructor 类,可以被读取。聊天记忆、智能体状态与其他编解码器不使用参数名,与 Jackson 2 一致。

无论编译器设置如何,要明确选择一个构造函数,请为它加注解:

  • 用 @JsonCreator(mode = JsonCreator.Mode.DELEGATING),以通过一个单参构造函数读取一个像 "abc" 这样的普通值;
  • 用 @JsonCreator,并为每个参数加上 @JsonProperty("name"),以通过一个带参构造函数读取一个对象。

两者在任一 Jackson 版本下都有效。

有意为之的不同

出于"恢复它们得不偿失"的考虑,有少数差异被有意保留:

  • 日期与时间以 ISO-8601 字符串写出。 java.util.Date 与 Calendar 会变成 "1970-01-01T00:00:00.000Z" 而非纪元毫秒。当 classpath 上有 jackson-datatype-jsr310(如 Spring Boot 与 Quarkus 应用)时,Jackson 2 也会把 Instant、OffsetDateTime 与 Duration 写成数字;本模块把它们写成 ISO-8601 字符串,而 LLM 读它们要可靠得多。对 Date、Instant 与 Duration,数字仍能被读取。
  • java.time.Month 被写成从 1 开始的数字(一月为 1)。在有 jackson-datatype-jsr310 时,Jackson 2 写 "JANUARY" 并把数字读成从 0 开始的位置,所以 1 是 FEBRUARY。两个版本都能读取名称。
  • 对枚举,"" 被读作 null,与缺失值相同。这也适用于必填枚举:像缺失字段一样,"" 给出 null 而非报错。Jackson 2 则会失败,但提供者会发送它——一个 OpenAI 兼容服务器对工具调用返回 "type": "" 正是暴露此问题的场景——而 LLM 对一个可选枚举可能回答 ""。对象、映射或列表的 "" 仍会失败,与 Jackson 2 一致。
  • 一个 private 单参构造函数不会被用来读取普通值。 Jackson 2 会用它;在 Jackson 3 下,请用 @JsonCreator(mode = JsonCreator.Mode.DELEGATING) 注解它。
  • 对于结构化输出与工具参数,当一个类同时具有无参构造函数与带参构造函数,且用 -parameters 编译时,会通过带参构造函数来创建它。 Jackson 2 会用无参构造函数然后设置字段。只有当那个构造函数做的不只是赋值字段时,这才有影响。
  • 一个像 xValue 且有 getter getXValue() 的字段会被写成 "xValue"。 Jackson 2 从 getter 派生出 "xvalue":对结构化输出、工具结果与智能体状态,它同时写 "xValue" 与 "xvalue",而对提供者请求只写 "xvalue"。由 Jackson 2 写出的、带有这样一个重复键的智能体状态,在本模块下读取会失败。

失败会带上一个 LangChain4j 类型。 默认情况下,一个 JSON 失败会表现为一个包装了 Jackson 2 自身异常的 RuntimeException——这意味着要对它作出反应的代码必须了解 Jackson 2。有了本模块,读写 JSON 失败时会改抛 JsonReadException 或 JsonWriteException,二者都是 LangChain4jException,并把该库的异常保留为 cause。

这是一次有意为之的推进而非不一致:带类型的异常是 LangChain4j 下一个大版本的方向,而 Jackson 2 的编解码器在此之前保持原样,好让既有代码继续工作。如果你通过 Jackson 类型来捕获 JSON 失败,那正是你添加本模块时唯一需要重新审视的地方:

java
- } catch (JsonParseException e) {
+ } catch (JsonReadException e) {

捕获 RuntimeException 在两种情况下都有效。

有两处会不经过 catch 块就到达应用代码,因此在切换之前值得检查:

一个工具参数错误处理器。 当模型产出的工具参数不是有效 JSON 时,LangChain4j 会把失败交给你通过 AiServices.toolArgumentsErrorHandler(...) 注册的处理器。在 Jackson 2 下,那个失败是 Jackson 自身的 JsonParseException;在本模块下它是 JsonReadException。一个依据类型分支的处理器会不再匹配,而且没有任何警告,因为处理器仍被调用、只是走了另一个分支:

java
AiServices.builder(Assistant.class)
    .toolArgumentsErrorHandler((error, context) -> {
-       if (error instanceof JsonParseException) {
+       if (error instanceof JsonReadException) {
            return ToolErrorHandlerResult.text("Please return valid JSON.");
        }
        throw error;
    })

如果你想让一个处理器在两种情况下都有效,请对消息、或对 RuntimeException 进行匹配。

OutputParsingException 的 cause。 当结构化输出无法解析时,无论哪种情况 LangChain4j 都抛 OutputParsingException——这个类型不变。变的是它底下的异常,从 Jackson 2 的 JsonProcessingException 变为 Jackson 3 的 StreamReadException。检查 getRootCause() 的代码需要同样对待。

你已经存储的数据仍可读取。 由 Jackson 2 写出的聊天记忆与 InMemoryEmbeddingStore 文件,能被 Jackson 3 正确读取,而 Jackson 3 写出的内容与 Jackson 2 本会写出的逐字节相同。这有测试覆盖,因此会一直保持。

移除 Jackson 2

添加本模块本身并不会移除 Jackson 2——它仍作为一个普通的传递依赖到来,二者可以无限期共存。如果你想彻底去掉它,通常能做到,因为一旦本模块存在,LangChain4j 就不再加载任何 Jackson 2 类。

它能否真正被移除,取决于你使用了哪些模块:

模块能排除 Jackson 2 吗?
langchain4j-core、langchain4j能——它们自带 Jackson 2 编解码器,但那些只是回退,本模块在 classpath 上时它们永不被加载
提供者模块——OpenAI、Anthropic、Mistral、Ollama、Gemini 及其他能
嵌入存储、网页搜索、代码执行能
langchain4j-agentic能
langchain4j-guardrails不能——JsonExtractorOutputGuardrail 有意用一个普通的 Jackson 2 ObjectMapper 来解析,好让护栏保持原有的严格
langchain4j-mcp不能——Jackson 的 JsonNode 出现在其公共 API 中,因此该模块在每条路径上都会加载 Jackson 2
langchain4j-vespa不能——它的 HTTP 客户端使用 Retrofit 自带的 Jackson 2 转换器
任何使用厂商 SDK 的——AWS、Azure、Google不能——该 SDK 本身就依赖 Jackson 2

Maven 的排除只作用于它所写在的那个依赖上,所以它们要加在你声明的每一个 LangChain4j 依赖上,而不只是第一个:

xml
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.22.0</version>
    <exclusions>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- and the same on langchain4j-open-ai, and on any other LangChain4j module you use -->

不要排除 com.fasterxml.jackson.core:jackson-annotations。它保留了 2.x 的坐标,但它是共享的:Jackson 3 依赖它并读取那些注解。

这套配置不只是"被认为可行",它在每个 pull request 上都会被构建并运行:integration-tests/integration-tests-jackson3 是一个完全按这种方式组装的应用——core、主模块、一个提供者、agents 与本模块,并从整个依赖图中排除了 Jackson 2——其测试覆盖 AI 服务结构化输出、工具调用、聊天记忆持久化、嵌入存储持久化与智能体状态持久化。如果其中任何一项开始需要 Jackson 2,那些测试就会因找不到该类而失败。

如果你使用 MCP、Vespa 或某个云 SDK,Jackson 2 会留下。这一 opt-in 仍会为 LangChain4j 自身序列化的所有内容提供单一的 Jackson 3 代码路径。

如果你编写一个提供者或一个 DTO

若你要贡献一个传输类型(wire type),有两件事要知道。

命名应放在编解码器上,而不是类型上。 @JsonNaming 位于 Jackson 2 的 databind 包,因此 Jackson 3 完全看不到它——字段名会悄无声息地输出为 camelCase。请改在编解码器上设置命名:

java
ProviderJson.codec(ProviderJsonSpec.builder()
        .propertyNaming(ProviderJsonSpec.PropertyNaming.SNAKE_CASE)
        .build());

如果单个字段需要不同的名字,@JsonProperty("...") 在两个版本下都有效,因为它来自 jackson-annotations——两个版本共享的那个 artifact。

一个基于 builder 的 DTO 需要在其 builder 上带 @JsonCreator 与字段可见性。 @JsonDeserialize(builder = ...) 也是一个 databind 注解,所以在 Jackson 3 下,该 DTO 改由通过那个接收 builder 的构造函数上的 @JsonCreator 来构建。Jackson 3 通过写 builder 的 private 字段来填充它,因此该 builder 还需要 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)。没有它,响应会无错误地解析,但字段会保持 null。三者都必须具备。

一个 builder 方法是否运行,取决于它身上的注解。 这是需要内化的部分,因为它是静默的,且规则不是你直觉的那样。Jackson 2 通过调用 builder 的 setter 再调用 build() 来填充它。Jackson 3 只有在 setter 带有属性标记时才调用它,否则直接写字段,且从不调用 build():

在 builder 的 setter 上Jackson 2Jackson 3
@JsonSetter("odd-name")、@JsonProperty、裸 @JsonSetter调用调用
单独的 @JsonAlias调用不调用——该别名被忽略
无注解调用不调用——字段被直接写入
build()调用从不调用

所以两个编解码器会在同一个类型内逐个属性地产生分歧,这正是此事值得定为一条规则、而非靠警觉的原因:

把类型所保证的东西放在两条路径都会经过的地方。 一个默认值应放在 builder 的字段上,而不是 build() 里。一次防御性拷贝或任何归一化应放在构造函数里,而不是某个 setter 里。一个别名需要同时加在 builder 的字段与其 setter 上的 @JsonAlias。

其中每一项都在此出过错:一个在 build() 里设默认值的类型丢掉了每一个 mistral-ai 工具调用;setter 上的 @JsonAlias 丢掉了来自 vLLM、OpenRouter 与 Groq 的 reasoning 内容;而一个未加注解的 setter 里的 unmodifiableList(...) 导致同一个响应在一个编解码器下返回可变、在另一个下不可变。它们都没有抛异常。

支持该 opt-in 的模块会声明一个 jackson3 Maven profile,把 langchain4j-jackson3 放到该模块的测试 classpath 上,从而让其现有测试针对 Jackson 3 运行。CI 在每个 pull request 上运行它们全部,你也可以用同样的方式运行单个模块:

bash
mvn test -Pjackson3 -pl langchain4j-your-module

在相信结果之前,请检查该模块的 pom.xml 确实声明了那个 profile:Maven 会忽略所选模块未声明的 profile,所以上面那条命令会在把一切都跑在 Jackson 2 上的情况下报告成功。添加该 profile 是迁移一个模块的一部分。

该 profile 把 langchain4j-core-jackson3 放到测试 classpath 上,而不是 langchain4j-jackson3,因为后者依赖 langchain4j 模块,会对那些本身构建在该模块之上的模块造成依赖图成环。InMemoryEmbeddingStore 持久化——即需要完整 artifact 的那部分——由 langchain4j-jackson3 自己的测试与 integration-tests/integration-tests-jackson3 覆盖。

langchain4j-open-ai 还带有一个 OpenAiBuilderCreatorParityTest,它把每个通过其 builder 构建的、基于 builder 的 DTO 与同一个 DTO 从 {} 解析出的结果作比较。这正是上文缺失的 build() 调用所造成的差异,因此该测试能一次性捕获整个 OpenAI 传输模型中的此类问题,而不是一次一个字段。同一个测试在某个基于 builder 的 DTO 缺少 @JsonCreator、或其 builder 缺少 @JsonAutoDetect 时也会失败。

如果你接入自己的 JSON

要使用 Jackson 3,你并不需要这一节——添加依赖就够了。它是为那些向 LangChain4j 提供自己配置好的 JSON mapper、而非让 LangChain4j 自行挑选的框架准备的,这正是 langchain4j-jackson3 自己所做的。

LangChain4j 没有单一的 JSON 入口。它在下面每个位置都向 ServiceLoader 询问一个编解码器,以便每个都能被分别应答:

Service 接口决定 LangChain4j 如何读写
dev.langchain4j.spi.json.JsonCodecFactory通用 JSON——AI 服务的结构化输出、模型的工具参数
dev.langchain4j.spi.json.ProviderJsonCodecFactory与 LLM 提供者交换的请求与响应
dev.langchain4j.spi.json.StateJsonCodecFactory类型事先未知、因而携带类型名的状态——智能体状态
dev.langchain4j.spi.data.message.ChatMessageJsonCodecFactory你持久化的聊天记忆
dev.langchain4j.spi.agent.tool.ToolSpecificationJsonCodecFactoryToolSpecification.toJson() 与 ToolSpecification.fromJson(String)
dev.langchain4j.spi.prompt.structured.StructuredPromptFactory@StructuredPrompt 模板
dev.langchain4j.spi.store.embedding.inmemory.InMemoryEmbeddingStoreJsonCodecFactoryInMemoryEmbeddingStore.serializeToJson()

最后一个位于 langchain4j;其余位于 langchain4j-core。它们全都标注了 @Internal,在这里意味着它们是面向集成而非应用的,且可能在次要版本之间变化。

框架自身的实现胜出。 如果已有东西提供了其中某一个——Quarkus 提供了四个——添加本模块并不会把它夺走。Jackson 3 的工厂声明的优先级低于其他一切,因此它们只作用于其他任何东西都未认领的那些 service。这一切静默发生:不会记录警告,因为优先级决定了使用哪个实现。这意味着在这样的框架上,该 opt-in 在设计上是部分的:提供者流量与智能体状态迁到 Jackson 3,而框架自身认领的那些 service 仍留在它自己的编解码器上。如果你想让整个应用都用 Jackson 3,请移除框架的这些注册。

要么全部实现,要么清楚你略过了哪些。 每一个都被独立解析,而某个没有注册实现的就回退到 Jackson 2。只应答其中一部分而不应答其余的并不是错误,也不会产生警告——它产生的是这样一个应用:比如聊天记忆由你的 mapper 写、而智能体状态由另一个库写。如果你有意略过某一个,回退就是你所得到的。

如果你已经与较旧版本集成,其中有两个值得再看一眼:ProviderJsonCodecFactory 与 StateJsonCodecFactory 是新增的,所以一个不了解它们的现有集成会继续工作,却在悄悄地为提供者流量与智能体状态使用 Jackson 2。

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。