Spring RestClient如何反序列化两种完全不同类型的第三方API响应
这个第三方API的设计确实有点坑——不管成功失败都返回200,还返回完全不同的JSON结构,手动抓异常重试确实麻烦。下面给你几个Spring生态内的优雅解决方案,不用写try-catch就能自动适配两种响应类型:
方案1:先转成JsonNode,根据字段判断再转换
这是最直观的方案:先把响应体转成Jackson通用的JsonNode(不绑定到具体POJO),然后通过判断标志性字段的存在性(比如device或message),再决定转成GoodPojo还是BadPojo。
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.web.client.RestClient; public String fetchApiResponse() { // 可以注入Spring管理的ObjectMapper,不需要手动new ObjectMapper objectMapper = new ObjectMapper(); JsonNode responseNode = restClient.get() .uri("https://the-api.com") .retrieve() .body(JsonNode.class); Object result; if (responseNode.has("device")) { // 存在device字段,解析为成功响应 result = objectMapper.treeToValue(responseNode, GoodPojo.class); } else if (responseNode.has("message")) { // 存在message字段,解析为错误响应 result = objectMapper.treeToValue(responseNode, BadPojo.class); } else { // 处理未知结构的边界情况 throw new RuntimeException("Unknown API response structure"); } return result.toString(); }
这个方案完全避开了异常捕获,通过字段存在性直接判断响应类型,代码逻辑清晰,实现成本极低。
方案2:定义通用响应类(利用Jackson的灵活绑定)
如果想更贴合面向对象的写法,可以定义一个包含所有可能字段的通用响应类,利用Jackson的特性:当JSON中不存在某个字段时,对应的属性会被设为null,再通过字段是否为null来区分响应类型。
首先定义通用响应类:
public record GenericResponse( String device, Float temperature, String message, Integer code ) { // 提供判断响应类型的方法 public boolean isSuccess() { return device != null; } // 转成具体POJO的方法 public GoodPojo toGoodPojo() { return new GoodPojo(device, temperature); } public BadPojo toBadPojo() { return new BadPojo(message, code); } }
然后直接调用API并转换:
public String fetchApiResponse() { GenericResponse genericResponse = restClient.get() .uri("https://the-api.com") .retrieve() .body(GenericResponse.class); Object result = genericResponse.isSuccess() ? genericResponse.toGoodPojo() : genericResponse.toBadPojo(); return result.toString(); }
这个方案把类型判断的逻辑封装在通用类中,代码更整洁,适合需要在多处复用响应处理逻辑的场景。
方案3:自定义Jackson类型解析器(全局自动适配)
如果需要在多个地方处理这种多类型响应,可以通过自定义Jackson的类型解析逻辑,让框架自动帮你判断并转换类型。这个方案实现复杂,但一旦配置好就可以全局复用。
步骤1:定义标记接口
让两个POJO实现同一个空接口,作为Jackson的类型判断入口:
public interface ApiResponse {} public record GoodPojo(String device, float temperature) implements ApiResponse {} public record BadPojo(String message, int code) implements ApiResponse {}
步骤2:自定义类型解析器
实现Jackson的TypeIdResolver和TypeDeserializer,根据JSON字段判断目标类型:
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonParser; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer; import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder; import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; import com.fasterxml.jackson.databind.type.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; import java.io.IOException; public class ApiResponseTypeIdResolver extends TypeIdResolverBase { private JavaType baseType; @Override public void init(JavaType baseType) { this.baseType = baseType; } @Override public String idFromValue(Object value) { return idFromValueAndType(value, value.getClass()); } @Override public String idFromValueAndType(Object value, Class<?> suggestedType) { return (value instanceof GoodPojo) ? "good" : "bad"; } @Override public JavaType typeFromId(DeserializationContext context, String id) { return "good".equals(id) ? context.constructType(GoodPojo.class) : context.constructType(BadPojo.class); } @Override public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.CUSTOM; } } public class ApiResponseTypeResolverBuilder extends StdTypeResolverBuilder { @Override public TypeDeserializer buildTypeDeserializer(DeserializationConfig config, JavaType baseType, Collection<NamedType> subtypes) { return new ApiResponseTypeDeserializer(baseType, this, null, "_type", false, null, config.getTypeFactory()); } private static class ApiResponseTypeDeserializer extends AsPropertyTypeDeserializer { public ApiResponseTypeDeserializer(JavaType baseType, TypeResolverBuilder<?> builder, TypeIdResolver idRes, String typePropertyName, boolean typeIdVisible, Class<?> defaultImpl, TypeFactory typeFactory) { super(baseType, builder, idRes, typePropertyName, typeIdVisible, defaultImpl, typeFactory); } @Override public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.readValueAsTree(); // 根据字段存在性判断类型ID String typeId = node.has("device") ? "good" : "bad"; // 重置解析器位置,让Jackson继续处理 p = ctxt.getParserFactory().createParser(node.toString()); p.nextToken(); return super.deserializeTypedFromObject(p, ctxt); } } }
步骤3:给标记接口添加Jackson注解
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonTypeResolver; @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, resolver = ApiResponseTypeIdResolver.class) @JsonTypeResolver(ApiResponseTypeResolverBuilder.class) public interface ApiResponse {}
步骤4:直接调用API
现在可以直接把响应体转成ApiResponse类型,Jackson会自动帮你判断并转换:
public String fetchApiResponse() { ApiResponse result = restClient.get() .uri("https://the-api.com") .retrieve() .body(ApiResponse.class); return result.toString(); }
这个方案实现了完全自动化的类型转换,但配置成本最高,适合需要在全局范围内处理多类型响应的场景。
方案选择建议
- 简单场景优先选方案1:代码量少,逻辑直观,不需要额外封装。
- 多场景复用优先选方案2:面向对象封装,代码可读性更高。
- 全局统一处理优先选方案3:一劳永逸,适合大型项目的统一API响应处理。
所有方案都不需要手动编写try-catch捕获转换异常,完全符合你的需求。
内容来源于stack exchange

