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 ConstraintViolationException
s 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
);
}
}