You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Spring Boot中使用Rest Client反序列化文本JSON问题

Spring Rest Client/Web Client 处理JSON字符串字面量响应问题

问题背景

调用外部REST API时,响应头Content-Type为application/json; charset=utf-8,但响应体并非标准JSON结构,而是JSON字符串字面量:

"{\"Code\":\"123-456\",\"Number\":1}"

而非预期的标准JSON:

{
  "Code": "123-456",
  "Number": 1
}

直接使用Rest Client的两种方式均报错:

方式一:直接转换为实体类

ApiResponse response = restClient.post()
    .uri(url)
    .contentType(MediaType.APPLICATION_JSON)
    .body(request)
    .retrieve()
    .body(ApiResponse.class);

方式二:转换为String类型

String response = restClient.post()
    .uri(url)
    .contentType(MediaType.APPLICATION_JSON)
    .body(request)
    .retrieve()
    .body(String.class);

两种方式均在.body(...)环节抛出反序列化错误:

Error while extracting response for type [com.example.test.models.ApiResponse] and content type [application/json;charset=utf-8]

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.lang.String` from Object value (token `JsonToken.START_OBJECT`)
at [Source: (jdk.internal.net.http.ResponseSubscribers$HttpResponseInputStream); line: 1, column: 1]

原因在于:框架根据application/json的响应头,期望解析标准JSON对象,但实际响应以双引号开头,属于字符串字面量,导致解析逻辑冲突。


解决方案

方案1:Rest Client 手动处理响应体

先获取原始字节数组,手动去除首尾引号、还原转义字符后,再用ObjectMapper解析为实体:

// 构建默认配置的Rest Client
RestClient restClient = RestClient.create();

// 获取原始响应字节
byte[] responseBytes = restClient.post()
    .uri(url)
    .contentType(MediaType.APPLICATION_JSON)
    .body(request)
    .retrieve()
    .body(byte[].class);

// 处理字符串字面量:去除首尾引号,还原转义字符
String rawJson = new String(responseBytes, StandardCharsets.UTF_8)
    .replaceAll("^\"|\"$", "")
    .replace("\\\\", "\\");

// 解析为实体类
ApiResponse response = new ObjectMapper().readValue(rawJson, ApiResponse.class);

方案2:Rest Client 自定义消息转换器

通过自定义MappingJackson2HttpMessageConverter,统一处理这种特殊格式的响应:

// 自定义消息转换器
MappingJackson2HttpMessageConverter customConverter = new MappingJackson2HttpMessageConverter() {
    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException {
        String rawStr = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
        // 判断是否为字符串包裹的JSON
        if (rawStr.startsWith("\"") && rawStr.endsWith("\"")) {
            String processedJson = rawStr.substring(1, rawStr.length() - 1)
                .replace("\\\\", "\\");
            return getObjectMapper().readValue(processedJson, clazz);
        }
        // 兼容正常JSON格式
        return super.readInternal(clazz, inputMessage);
    }
};

// 构建带自定义转换器的Rest Client
RestClient restClient = RestClient.builder()
    .messageConverters(converters -> {
        // 移除默认的Jackson转换器
        converters.removeIf(c -> c instanceof MappingJackson2HttpMessageConverter);
        // 添加自定义转换器
        converters.add(customConverter);
    })
    .build();

// 正常调用即可自动解析
ApiResponse response = restClient.post()
    .uri(url)
    .contentType(MediaType.APPLICATION_JSON)
    .body(request)
    .retrieve()
    .body(ApiResponse.class);

方案3:Web Client 异步处理响应

通过exchangeToMono手动处理响应体:

WebClient webClient = WebClient.create();

Mono<ApiResponse> responseMono = webClient.post()
    .uri(url)
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue(request)
    .exchangeToMono(clientResponse -> {
        return clientResponse.bodyToMono(String.class)
            .map(rawStr -> {
                String processedJson = rawStr.substring(1, rawStr.length() - 1)
                    .replace("\\\\", "\\");
                try {
                    return new ObjectMapper().readValue(processedJson, ApiResponse.class);
                } catch (JsonProcessingException e) {
                    throw new RuntimeException("解析响应失败", e);
                }
            });
    });

// 同步获取结果(异步场景可使用subscribe)
ApiResponse response = responseMono.block();

方案4:Web Client 自定义解码器

自定义Jackson2JsonDecoder统一处理特殊响应格式:

ObjectMapper objectMapper = new ObjectMapper();
Jackson2JsonDecoder customDecoder = new Jackson2JsonDecoder(objectMapper) {
    @Override
    public Object decode(DataBuffer dataBuffer, ResolvableType targetType, MimeType mimeType, Map<String, Object> hints) throws DecodingException {
        String rawStr = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()).toString();
        if (rawStr.startsWith("\"") && rawStr.endsWith("\"")) {
            String processedJson = rawStr.substring(1, rawStr.length() - 1)
                .replace("\\\\", "\\");
            try {
                return objectMapper.readValue(processedJson, targetType.getType());
            } catch (JsonProcessingException e) {
                throw new DecodingException("解码包裹式JSON失败", e);
            }
        }
        return super.decode(dataBuffer, targetType, mimeType, hints);
    }
};

// 构建带自定义解码器的Web Client
WebClient webClient = WebClient.builder()
    .codecs(configurer -> configurer.customCodecs().register(customDecoder))
    .build();

// 正常调用自动解析
ApiResponse response = webClient.post()
    .uri(url)
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue(request)
    .retrieve()
    .bodyToMono(ApiResponse.class)
    .block();

为何会出现这种返回格式?

这种非标准格式通常不是有意设计,而是以下原因导致的:

  • 历史遗留问题:早期系统错误地将JSON对象序列化为字符串后返回,未修正Content-Type或序列化逻辑
  • 中间层处理失误:网关、代理等中间服务在转发响应时,额外对JSON做了字符串包裹处理
  • 序列化配置错误:后端框架的序列化工具被错误配置,导致JSON对象先转为字符串再输出
  • 旧客户端兼容:为适配只能处理字符串格式响应的旧客户端,后端被迫保留该格式

内容的提问来源于Stack Exchange,提问作者Gonzalo Martinez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 00:04:57