I am currently struggling with seemingly inconsistent behavior when dealing with request body validation in Spring REST Controllers.
Validation of singular entities result in a MethodArgumentNotValidException. Validation of Collections of the same entity class result in a ConstraintViolationException. My understanding is that the MethodArgumentNotValidException is the expected result during handling of Spring REST controllers, while ConstraintViolationExceptions are expected when Bean validation is done by Hibernate or other frameworks. However, in this case both occur while validating @RequestBody annotated entities.
Is there an explanation for this inconsistency? Furthermore, can I unify application behavior in any way? Handling of two different exception types translating validation exceptions completely differently is not ideal.
Example:
Controller:
@Validated
@RestController
@Slf4j
public class TestController {
@PostMapping(path = "/test-entities")
public ResponseEntity<Void> saveTestEntity(@Valid @RequestBody List<TestEntity> testEntities) {
log.info("Received: {}", testEntities);
return ResponseEntity.noContent()
.build();
}
@PostMapping(path = "/test-entity")
public ResponseEntity<Void> saveTestEntity(@Valid @RequestBody TestEntity testEntity) {
log.info("Received {}", testEntity);
return ResponseEntity.noContent()
.build();
}
}
Test Entity:
@Data
public class TestEntity {
@NotEmpty String foo;
}
Controller Advice:
@ControllerAdvice
public class TestControllerAdvice {
@ExceptionHandler(Exception.class)
ResponseEntity<String> handleException(Exception exception) {
return ResponseEntity.badRequest()
.body(exception.getClass().getSimpleName());
}
}
Test of Described Behavior
@WebMvcTest(controllers = TestController.class)
class ValidationExceptionsIssueApplicationTests {
@Autowired
MockMvc mockMvc;
String expectedException = MethodArgumentNotValidException.class.getSimpleName();
@Test
void assertThatNoExceptionIsReturned() throws Exception {
mockMvc.perform(post("/test-entities")
.contentType(MediaType.APPLICATION_JSON)
.content("""
[
{"foo": "bar"}
]
"""))
.andExpect(status().isNoContent());
}
@Test
void whenSingleEntityIsSent_thenMethodArgumentNotValidExceptionIsThrown() throws Exception {
mockMvc.perform(post("/test-entity")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"foo": ""}
"""))
.andExpectAll(
status().isBadRequest(),
content().string(expectedException)
);
}
@Test
void whenArrayOfEntitiesAreSent_thenMethodArgumentNotValidExceptionIsThrown() throws Exception {
mockMvc.perform(post("/test-entities")
.contentType(MediaType.APPLICATION_JSON)
.content("""
[
{"foo": ""}
]
"""))
.andExpectAll(
status().isBadRequest(),
content().string(expectedException) // <-- fails
);
}
}