Hello, this seems like an obvious duplicate of https://github.com/spring-projects/spring-framework/issues/19935 but I'd like to open it as a new issue as there's an important distinction. It happens in a scenario where no JavaBeans convention is present with the use of records:
@Validated
@RestController
public class CarController {
@PostMapping("/send")
public void validate(@RequestBody @Valid Car request) {
}
record Car(
Optional<List<@Valid Driver>> images
) {
}
record Driver(
@NotBlank
String name
) {
}
}
@TestInstance(PER_CLASS)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class ValidationTest {
@LocalServerPort
public int port;
@BeforeAll
void setUpRestAssured() {
RestAssured.port = port;
}
@Test
void shouldValidate() {
// given
var givenRequest = new CarController.Car(Optional.of(List.of(new CarController.Driver(""))));
// when
var response = RestAssured.with()
.contentType(ContentType.JSON)
.body(givenRequest)
.when()
.post("/send");
// then
then(response.statusCode()).isEqualTo(400);
}
}
Now I do understand that there are workarounds:
1. model the model in such a way that you merge the 3 states the model can be in into 2 (null, empty, values -> empty, values)
- this can work in some domains but might not in others
1. since this code works with regular @Validated
methods one can catch InvalidPropertyException
and pass the request body to a @Validated
method (I'd really like to avoid doing something like this).
1. don't use Optional in such cases and handle it in the code base and risk NPEs because of inconsistency
For me personally none of these are acceptable and are putting the Spring user in a bad spot and cutting his options short for what appears to be no good reason. Records are here and are going to stay here and are not connected to the Java Beans convention in any way. Optionals are here and are going to stay here and are not going away.
Comment From: lpandzic
Changed the name - note that this code does work for valid request, but for invalid requests it falls apart with 500 instead of 400.
Comment From: lpandzic
For anyone looking for a workaround here's what I used:
@AllArgsConstructor
@Aspect
public class ValidatorAdapterAspect {
private final Validator validator;
@Pointcut("within(org.springframework.boot.autoconfigure.validation.ValidatorAdapter)")
public void validatorAdapter() {
}
@Pointcut("execution(public void validate(Object, org.springframework.validation.Errors))")
public void validate() {
}
@Pointcut("validatorAdapter() && validate()")
public void validatorAdapterValidate() {
}
@Around("validatorAdapterValidate()")
public void doValidate(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
try {
proceedingJoinPoint.proceed();
} catch (InvalidPropertyException e) {
throw new InvalidPropertyBeanValidationException(validator.validate(proceedingJoinPoint.getArgs()[0]));
}
}
}
Afterwards I use regular exception handler infrastructure to deal with InvalidPropertyBeanValidationException.
Comment From: snicoll
Let's handle this as a duplicate of https://github.com/spring-projects/spring-framework/issues/19935, the issues are linked now so we'll review the sample when we get to it.