JSON 响应已使用 Jackson 和 JAX-RS 异常映射器转义引号

JSON Response Has Escaped Quotations Using Jackson and JAX-RS Exception Mapper

我有一个简单的要求,如果应用程序遇到异常,我的 JAX-RS Rest 端点应该 return 具有 500 HTTP header 状态的自定义 JSON 响应。

构建响应所需的数据来自具有多个属性的 object(见下文)。问题是,我只对每个 属性(几十个)中的一个或两个值感兴趣。而且我无法修改这些 models/classes 中的任何一个(有些具有用于 JSON 处理的 Jackson 注释,例如,在序列化期间应丢弃 null 属性)。

public class MainObject {
  private FirstProperty firstProperty;
  private SecondProperty secondProperty;
  private ThirdProperty thirdProperty;
  // other codes
  public String toString() {
    ObjectMapper mapper = new ObjectMapper();
    try { return mapper.writeValueAsString(this); }
    catch (Exception e) {  return null; }
  }
}

public class FirstProperty {
  private boolean bol = true;
  private double dob = 5.0;
  private List<String> subProperty;
  // other properties
  public String toString() {
    ObjectMapper mapper = new ObjectMapper();
    try { return mapper.writeValueAsString(this); }
    catch (Exception e) {  return null; }
  }
}

@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class SecondProperty {
  private String str;
  private List<String> subProperty;
  // other properties
  public String toString() {
    ObjectMapper mapper = new ObjectMapper();
    try { return mapper.writeValueAsString(this); }
    catch (Exception e) {  return null; }
  }
}

@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class ThirdProperty {
  private int intProp = true;
  private List<String> subProperty;
  // other properties
  public String toString() {
    ObjectMapper mapper = new ObjectMapper();
    try { return mapper.writeValueAsString(this); }
    catch (Exception e) {  return null; }
  }
}

我应该看到返回的预期 JSON 在客户端(例如,浏览器 -- 在 Edge 中测试):

{
  "firstProperty" : { "subProperty" : [ "val1" ] },
  "secondProperty" : { "str" : "val2", "subproperty" : [ "val3", "val6" ] },
  "thirdProperty" : { "subProperty" : [ "val4" ] }
}

相反,我所有的字段名称及其值都将引号转义,并在整个值周围加上额外的双引号,例如:

{
  "firstProperty" : "{ \"subProperty\" : [ \"val1\" ] }",
  "secondProperty" : "{ \"str\" : \"val2\", \"subproperty\" : [ \"val3\", \"val6\" ] }",
  "thirdProperty" : "{ \"subProperty\" : [ \"val4\" ] }"
}

请注意大括号前后的额外 "。我的环境是:

Java 1.8.45
FasterXML Jackson 2.9.8
Spring Boot 2.0.1
RestEasy (JBoss) JAX-RS
JBoss 6.4

我删除了代码中的大部分 "noise" 以查看发生这种情况的时间点。这是控制器:

@Path("/")
public class MainController {

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Path("/rest/path")
  public MainObject getMainObject throws MyCustomException {
    // A service call that throws MyCustomException
  }
}

和 JAX-RS ExceptionMapper 我发回响应的地方:

@Provider
public class MyCustomExceptionMapper extends ExceptionMapper<MyCustomException> {

  @Override
  public Response toResponse(MyCustomException ex) {
    HashMap<String, Object> responseBody = new HashMap<>();

    String strEx = ex.getStrEx(); // Comes from SecondProperty.str stored in MyCustomException, not that it matters

    // Instantiate an empty object that contains 
    MainObject obj = new MainObject();
    obj.getFirstProperty().setSubProperty(ex.getStrs());
    obj.getSecondProperty().setStr(strEx);
    obj.getSecondProperty().setSubProperty(ex.getStrs());
    obj.getThirdProperty().setSubProperty(ex.getStrs());

    responseBody.put("firstProperty", serializeFirstProperty(obj.getFirstProperty()));
    responseBody.put("secondProperty", serializeSecondProperty(obj.getSecondProperty()));
    responseBody.put("thirdProperty", serializeThirdProperty(obj.getThirdProperty()));

    Response response = Response.status(/* 500 status */).entity(responseBody).build();

    return response;
  }

}

因为我只需要从我的每个类型发回整体属性的一个非常小的子集,所以我创建了一个自定义 StdSerializer,它只会填充所需的 属性。为简洁起见,我只做 serializeFirstProperty() 但它们或多或少是相同的:

private StdSerializer<FirstProperty> getFPSerializer(FirstProperty firstProperty) {
  return new StdSerializer<FirstProperty>(FirstProperty.class) {
    @Override
    public void serialize(FirstProperty value, JsonGenerator gen, SerializerProvider provider) throws IOException {

      gen.writeStartObject();
      if (/* there are items in FirstProperty.subProperty */) {
        gen.writeArrayFieldStart("subProperty");
        for (String str : value.getSubProperty()) {
          gen.writeString(str);
        }
        gen.writeEndArray();
      }
      gen.writeEndObject();
    }
}

private <T> ObjectMapper getCustomOM(StdSerializer<?> serializer) {
  ObjectMapper mapper = new ObjectMapper();

  SimpleModule sm = new SimpleModule();
  sm.addSerializer(serializer);
  mapper.registerModule(module);

  return mapper;
}

然后使用这些辅助方法,例如:

private String serializeFirstProperty(FirstProperty firstProperty) {
  ObjectMapper mapper = getCustomOM(getFPSerializer(firstProperty));

  String ser = null;
  try { ser = mapper.writeValueAsString(firstProperty); }
  catch (JsonProcessingException e) { return null; }
  return ser;
}

我用 ObjectMapper 尝试了无数种配置,例如disable(JsonParser.Feature.ALLOW_BACKLASH_ESCAPING_ANY_CHARACTER)(找不到 JsonGenerator 的任何相关标志,我真的想以类似的方式禁用它)。

或从 serializeFirstProperty() 中显式 returning Object,或在 [=34] 中将 serializeFirstProperty() 中的所有 \" 替换为 " =] 是 returned.

或设置自定义 StdSerializerJsonGenerator.setCharacterEscapes(new CharacterEscapes() { //... } 或使用 JAX-RS Response 无济于事。我似乎总是得到带引号的 "string" 值,例如:

"firstProperty" : "{ \"subProperty\" : [ \"val1\" ] }"

如果我只是做

responseBody.put("firstProperty", mapper.writeValueAsString(obj.getFirstProperty()));

这会以某种方式产生正确的 JSON 输出,但是,它包含许多我不希望在此异常处理案例中使用的不必要的属性。

有趣的是,当我查看 response(或 responseBody 地图)时,一切看起来都是正确的(我没有看到带有双引号的值)。

另请注意,不仅我无法修改模型,而且它们的某些属性在创建过程中以默认值实例化,因此 not-null 包含不起作用,它们将出现在final JSON 如果我不使用自定义序列化。

有谁知道导致这种转义和额外引号的原因吗?

我想我在第一次尝试回答这个问题时误解了这个问题。

问题是您将 属性 序列化为字符串(使用 mapper.writeValueAsString(this) 然后将其添加到您认为是字符串的 responseBody json object map 但它是一个字符串到 Java object map。在你的例子中,在运行时它是一个字符串映射到另一个字符串(序列化的 json 对象表示为 Java 字符串)并且 Java 字符串也是一个 Java 对象。

您要做的是构造一个 Java 对象 responseBody 而不是地图。它应该充当具有所有特定属性等的 DTO,然后使用映射器在一个操作中将其序列化。因为如果您首先将 属性 序列化为 json 字符串,那么从 Java 的角度来看,它只是一个字符串,映射器没有机会将其解释为 json对象。