Search code examples
javajsonjax-rsresteasyjackson2

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


I have a simple requirement where, if application encounters an exception, my JAX-RS Rest endpoint should return a custom JSON response with 500 HTTP header status.

Data needed to construct the response comes from an object with several properties (see below). The problem is, I am only interested in one or two values from each property (out of several dozens). And I cannot modify any of these models/classes (some have a Jackson annotation for JSON processing, e.g. null properties should be discarded during serialization).

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; }
  }
}

The expected JSON that I should be seeing coming back is on the client side (say, a browser -- testing in Edge):

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

Instead, all my field names and their values have their quotations escaped, and extra double quotes around the entire value, e.g.:

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

Please note the extra " before and after the curly brackets. My environment is:

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

I eliminated the majority of "noise" in the code to see at what point this happens. This is the controller:

@Path("/")
public class MainController {

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

And JAX-RS ExceptionMapper where I send the response back:

@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;
  }

}

Since I only need to send back a very small subset of overall properties from each of my types, I created a custom StdSerializer that would only populate a needed property. For brevity, I only do serializeFirstProperty() but they are all more or less identical:

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;
}

Then use these helper methods like:

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;
}

I have tried countless of configurations with ObjectMapper, e.g. disable(JsonParser.Feature.ALLOW_BACKLASH_ESCAPING_ANY_CHARACTER) (couldn't find any relevant flag for JsonGenerator which I really want to disable in a similar fashion).

Or explicitly returning Object from serializeFirstProperty(), or replacing all the \" with " in serializeFirstProperty() when ser is returned.

Or set custom StdSerializer's JsonGenerator.setCharacterEscapes(new CharacterEscapes() { //... } or play around with JAX-RS Response at no avail. I always seem to get a "string" value with quotations, e.g.:

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

If I simply just do

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

somehow this produces the right JSON output, however, it includes a lot of unnecessary properties which I don't want in this exception handling case.

Funny thing is, when I peer into response (or responseBody map), everything looks right (I don't see values having double quotations).

Please also note that not only I can't modify the models, but some of their properties are instantiated during creation with default values, so not-null inclusion doesn't work, and they will appear in the final JSON if I don't use a custom serialization.

Does anyone know what's causing this escaped and extra quotations?


Solution

  • I think I misunderstood the question in the first attempt of answering it.

    The problem is that you serialize a property as string (using mapper.writeValueAsString(this) and then add it to the responseBody which you think of being string to json object map but it is a string to Java object map. In your case at runtime it is a string mapping to another string (the serialized json object is represented as Java string) and a Java string is a Java object as well.

    What you want to do instead is to construct a Java object responseBody instead of a map. It should act as a DTO having all the specific properties etc and then serializing it in in one action using the mapper. Because if you first serialize a property to a json string than it it is just a string from the Java point of view and the mapper has no chance to interpret it as a json object.