Jackson与JAX-RS ExceptionMapper返回JSON遇转义引号问题排查
嘿,这个问题我之前在JAX-RS项目里踩过一模一样的坑!咱们先拆解下问题根源,再给你几个适配你场景的解决办法。
问题根源:两次序列化导致的转义冗余
你看到的"firstProperty" : "{ \"subProperty\" : [ \"val1\" ] }"这种格式,本质是Jackson做了两次序列化:
- 你先用
ObjectMapper把FirstProperty对象转成了JSON字符串; - 然后把这个字符串塞进了响应POJO的
firstProperty字段里; - 最后JAX-RS在构建Response时,又对整个响应POJO做了一次Jackson序列化——这时候原本的JSON字符串就被当成普通文本处理,自动加上了外层引号,内部的引号也被转义了。
简单说就是:你把JSON当成字符串存,Jackson就把它当字符串序列化,自然就会出现多余的引号和转义符。
解决方案:避免两次序列化
针对你不能修改模型类的限制,这里有几个靠谱的方案:
方案1:直接构建JsonNode返回,跳过字符串中转
用Jackson的JsonNode来组装你需要的响应结构,直接返回JsonNode对象,让JAX-RS一次性完成序列化,不会有二次转义的问题:
@Override public Response toResponse(MyCustomException ex) { ObjectMapper mapper = new ObjectMapper(); ObjectNode errorResponse = mapper.createObjectNode(); // 获取原始的MainObject实例 MainObject mainObj = ex.getMainObject(); FirstProperty firstProp = mainObj.getFirstProperty(); // 只提取你需要的子属性,比如subProperty JsonNode targetSubNode = mapper.valueToTree(firstProp.getSubProperty()); errorResponse.set("firstProperty", targetSubNode); // 还可以加其他自定义字段,比如错误信息 errorResponse.put("errorMessage", "服务器内部错误"); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .type(MediaType.APPLICATION_JSON) .entity(errorResponse) .build(); }
这种方式完全绕开了中间字符串的生成,直接操作JSON节点,既保证了只返回需要的属性,又不会出现转义问题。
方案2:动态过滤模型类属性,返回过滤后的POJO
如果你还是想用响应POJO来返回,可以用Jackson的Mixin动态注解+属性过滤器,在不修改原模型类的前提下,过滤掉不需要的属性:
首先定义一个Mixin类,用来给目标模型类绑定过滤器:
@JsonFilter("FirstPropertyFilter") public abstract class FirstPropertyMixin {}
然后在异常映射器里配置ObjectMapper,动态应用过滤器:
@Override public Response toResponse(MyCustomException ex) { ObjectMapper mapper = new ObjectMapper(); // 给FirstProperty类动态添加Mixin注解 mapper.addMixIn(FirstProperty.class, FirstPropertyMixin.class); // 创建过滤器:只保留subProperty属性 SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("subProperty"); FilterProvider filterProvider = new SimpleFilterProvider().addFilter("FirstPropertyFilter", filter); // 构建自定义响应POJO,直接放入原始的FirstProperty对象 CustomErrorResponse errorResp = new CustomErrorResponse(); errorResp.setFirstProperty(ex.getMainObject().getFirstProperty()); errorResp.setErrorMessage("服务器内部错误"); // 用带过滤器的Writer序列化,避免冗余属性 String json = mapper.writer(filterProvider).writeValueAsString(errorResp); return Response.status(500) .type(MediaType.APPLICATION_JSON) .entity(json) .build(); }
这里关键是用Mixin给原模型类动态加上@JsonFilter,然后通过过滤器只保留你需要的属性,最后直接序列化响应POJO,不会有二次转义。
方案3:直接返回JSON字符串,明确媒体类型
如果你必须先序列化出指定属性的JSON字符串,那一定要确保JAX-RS不会再对这个字符串做序列化——直接把字符串作为实体返回,并指定媒体类型为application/json:
@Override public Response toResponse(MyCustomException ex) { ObjectMapper mapper = new ObjectMapper(); FirstProperty firstProp = ex.getMainObject().getFirstProperty(); // 只序列化需要的子属性,比如subProperty String targetJson = mapper.writeValueAsString(firstProp.getSubProperty()); // 组装成最终的响应JSON结构 String finalJson = String.format("{\"firstProperty\":%s,\"errorMessage\":\"服务器内部错误\"}", targetJson); return Response.status(500) .type(MediaType.APPLICATION_JSON) .entity(finalJson) .build(); }
这种方式要注意手动组装JSON时的格式正确性,适合简单的响应结构,复杂结构还是推荐前两种方案。
验证小技巧
你可以在本地测试时,打印一下序列化后的字符串,看看是否已经是正确的JSON格式。如果打印出来的是{"firstProperty":{"subProperty":["val1"]}},那直接返回这个字符串并指定媒体类型就不会有问题;如果打印出来的是带外层引号的"{\"firstProperty\":...}",那肯定是哪里做了二次序列化,得排查代码里的序列化步骤。
内容的提问来源于stack exchange,提问作者Malvon

