I find that with Spring Boot 2.0.3, the JSON error message passed to the exception handler and returned to frontend will be formatted as a String instead of a JSON and escaped with \
before every double quotation mark(i.e., {"foo":"bar"}
will be "{\"foo\":\"bar\"}"
.
To be concrete, I have a method converting a java.util.Map
into a JSON string. The method is:
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class Utilities {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static String jsonBuilder(Map<String, Object> resultMap) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.createObjectNode();
for (String key: resultMap.keySet()) {
Object value = resultMap.get(key);
if (value instanceof String) {
node.put(key, (String)value);
} else if (value instanceof Set) {
ArrayNode arrayNode = mapper.createArrayNode();
((Set) value).forEach(e -> arrayNode.add(e.toString()));
node.set(key, arrayNode); //put() is deprecated
}
}
return mapper.writeValueAsString(node);
}
}
And this test:
import static org.assertj.core.api.Assertions.assertThat;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
public class UtilitiesTests {
@Test
public void testJsonBuilder() throws JsonProcessingException {
Map<String, Object> map = new HashMap<String, Object>();
map.put("key1", "value1");
String result = Utilities.jsonBuilder(map);
assertThat(result).isEqualTo("{\"key1\":\"value1\"}");
Map<String, Object> map1 = new HashMap<String, Object>();
Set<String> set = new LinkedHashSet<String>(); //we must retain order.
set.add("arrVal1");
set.add("arrVal2");
map1.put("key2", "value2");
map1.put("key3", set);
String result1 = Utilities.jsonBuilder(map1);
assertThat(result1).isEqualTo("{\"key2\":\"value2\",\"key3\":[\"arrVal1\",\"arrVal2\"]}");
}
}
The test always passes, i.e., the method works well and can translate a map into a correct JSON.
Now, if I return directly from some @RequestMapping
method to the RESTful endpoint(testing from Postman), the returned value is a JSON, without any quotation.
Something like:
List<BinInfo> founds = repository.findAllByBin(bin);
BodyBuilder builder = ResponseEntity.status(HttpStatus.OK);
builder.contentType(MediaType.APPLICATION_JSON_UTF8);
if (founds != null && !founds.isEmpty() && founds.size() == 1) {
return builder.body(founds.get(0));
} else {
errors.put("error", "BIN not found");
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(Utilities.jsonBuilder(errors));
}
Will return:
{
"error": "BIN not found"
}
But, if I pass a wrongly formatted argument to the method, an exception org.springframework.web.bind.MethodArgumentNotValidException
will happen and will be caught by this exception handler:
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, WebRequest request) throws JsonProcessingException {
BindingResult result = ex.getBindingResult();
final List<FieldError> fieldErrors = result.getFieldErrors();
final Set<String> errors = new HashSet<>();
for (FieldError fe: fieldErrors) {
errors.add(fe.getField());
}
Map<String, Object> resultMap = new HashMap<String, Object>();
resultMap.put("error", "validation");
resultMap.put("fields", errors);
log.error("Validation error. ", ex);
return ResponseEntity.badRequest()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(Utilities.jsonBuilder(resultMap));
}
And, the returned will not be a JSON but a quoted and escaped string, like:
"{\"error\":\"validation\"}"
I know the exception is caught because I see these in the log:
[NODE=xxxxxx] [ENV=dev] [SRC=UNDEFINED] [TRACE=] [SPAN=] [2018-08-21T12:43:50.722Z] [ERROR] [MSG=[XNIO-2 task-2] c.p.b.controller.BinInfoController - Validation error. org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument at index 0 in method: public org.springframework.http.ResponseEntity<java.lang.Object> com.xxxxx.binlookup.controller.BinInfoController.insertBIN(com.xxxxxx.binlookup.model.BinInfo) throws com.fasterxml.jackson.core.JsonProcessingException, with 1 error(s): [Field error in object 'binInfo' on field 'bin': rejected value [11]; codes [Size.binInfo.bin,Size.bin,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [binInfo.bin,bin]; arguments []; default message [bin],8,6]; default message [?????6?8??]]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:138)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:131)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
...
How can I prevent this?
Is there some interceptor between the handler and the endpoint?