WebClient 的 ProblemDetail 扩展字段丢了:检查 Jackson 3 解码器
Spring 7.0.9 默认 WebClient 解码 ProblemDetail 时,标准字段正常而扩展成员丢失。为实际使用的 Jackson 3 解码器注册 mixin,保留已有 mapper 配置;Boot 自动配置的客户端须分别判断。
服务端返回的 application/problem+json 明明有 errorCode、orderId,WebClient 取到的 ProblemDetail 却只有 status、title、detail 等标准字段,getProperties() 为 null。若客户端直接使用 WebClient.create() 或未定制 codec 的 builder,先检查 Jackson 3 解码器有没有注册 ProblemDetail 的 mixin。
明确的版本对比是 Spring Framework 7.0.9、Spring Boot 4.1.1、Jackson 3 丢失扩展字段,而 Framework 6.2.11、Boot 3.5.6、Jackson 2 的相同读取保留它们。不能据此把所有 Boot 4 客户端都判为受影响:Boot 自动配置的 WebClient.Builder 可以带有已经注册 mixin 的 mapper,与自行创建的默认客户端不同。
标准字段能读,不代表扩展属性也有接收入口
ProblemDetail 的扩展成员在 JSON 顶层,与 status 等字段并列。它们不是名为 properties 的嵌套对象;不应为了迁就客户端,先把服务端协议改成另一种结构。
Spring 的 ProblemDetailJacksonMixin 通过 @JsonAnySetter 把未知成员收进属性 map,并通过 @JsonAnyGetter 在编码时展开。7.0.10 正式源码中的 JacksonJsonDecoder 默认构造和 builder 构造没有补上这一 mixin,仍需要检查实际 mapper 的配置。升级到该版本不能据此保证自动恢复扩展字段。
RestClient 使用的转换器、编码响应的 encoder,以及 WebClient 的 decoder 是不同入口。写出的 JSON 正确,或 RestClient 能读,并不能证明当前 WebClient 的响应解码 mapper 也已经配置好。
在真正使用的解码器上补齐 mixin
继续沿用项目已经定制的 mapper builder 和 WebClient builder,在安装默认 JSON 解码器时加入 mixin。以下是配置位置的片段,mapperBuilder 为已有 JsonMapper.Builder,webClientBuilder 是稍后实际构建并注入客户端的 builder:
import org.springframework.http.ProblemDetail;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.converter.json.ProblemDetailJacksonMixin;
mapperBuilder.addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class);
webClientBuilder.codecs(configurer -> configurer.defaultCodecs()
.jacksonJsonDecoder(new JacksonJsonDecoder(mapperBuilder)));
Jackson 3 的 JsonMapper 位于 tools.jackson.databind.json,不要误用 Jackson 2 的同名类型。已有模块、日期格式、命名策略和必要限制应在这份 builder 上保留;若原来已经安装自定义 decoder,应合并其配置,不能新建一个空 mapper 覆盖业务约定。连接、认证、超时等客户端设置也继续沿用。
如果项目已使用 Boot 注入且配置正确的 builder,先确认没有在调用链中另建一个 WebClient.create()。修改一个没有被消费的配置对象,不会影响真正发出的请求。
同时核验成功响应与错误响应
保留标准字段和两个不同类型的扩展成员作为对照,确认字符串 errorCode 与数值 orderId 都进入 properties,标准字段和状态码也没有改变。实际业务若通过 WebClientResponseException.getResponseBodyAs(ProblemDetail.class) 读取错误体,就验证这条入口,不只单独调用 mapper 解析一段字符串。
还要检查其他 JSON 响应、原有模块和内存限制仍按项目约定工作。不要通过忽略未知字段、吞掉空 properties 或在所有错误上填入固定 errorCode 来让判断通过;那些做法仍会丢掉服务端给出的具体错误信息。


