Handling of MethodArgumentNotValidException overridden by DefaultErrorAttributes - spring

My goal is to have a custom response body for validation errors. This is a very common case and I've read lots of posts/blogs/articles and I've even implemented this myself in the past. For some reason, I cannot figure this out.
I have this #RestControllerAdvice
#Slf4j
#RequiredArgsConstructor
#RestControllerAdvice
public class ErrorHandler extends ResponseEntityExceptionHandler {
private final MessageSource messageSource;
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
ValidationErrorDTO validationError = ValidationErrorDTO.builder()
.fieldErrors(ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> FieldErrorDTO.builder()
.field(fieldError.getField())
.message(messageSource.getMessage(fieldError, LocaleContextHolder.getLocale()))
.build())
.collect(Collectors.toList()))
.build();
LOGGER.debug("W3MnsZ validation error: {}", validationError);
return new ResponseEntity<Object>(validationError, HttpStatus.BAD_REQUEST);
}
}
====
#ToString
#Builder
public class ValidationErrorDTO {
private final List<FieldErrorDTO> fieldErrors;
}
====
#ToString
#Builder
#Getter
#JsonInclude(Include.NON_NULL)
public class FieldErrorDTO {
private final String field;
private final String message;
}
My ErrorHandler.handleMethodArgumentNotValid() gets hit, but the actual response body returned to the client is not from my ValidationErrorDTO.
{
"timestamp": 1523115887261,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"errors": [
{
"codes": [
"NotEmpty.myDTO.field.another.lastly",
"NotEmpty.field.another.lastly",
"NotEmpty.lastly",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"myDTO.field.another.lastly",
"field.another.lastly"
],
"arguments": null,
"defaultMessage": "field.another.lastly",
"code": "field.another.lastly"
}
],
"defaultMessage": "may not be empty",
"objectName": "myDTO",
"field": "field.another.lastly",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='myDTO'. Error count: 1",
"path": "/myPath"
}
I've figured out that what's happening is org.springframework.boot.autoconfigure.web.DefaultErrorAttributes is getting hit and somehow overriding my custom response body.
What do I do to allow my custom response body to be returned to the client?

Just figured it out.
ValidationErrorDTO.fieldErrors had no getter.
#ToString
#Builder
#Getter
public class ValidationErrorDTO {
#Singular
private final List<FieldErrorDTO> fieldErrors;
}
Now it works and we get response:
{
"fieldErrors": [
{
"field": "field.another.lastly",
"message": "may not be empty"
}
]
}

Related

with #SpringBootTest (or #WebfluxTest), the errors attribute disappears

I made very simple controller like below.
#PostMapping("/books")
public void create(#Valid #RequestBody BookPayload bookPayload) {
}
#Getter
#Setter
public class BookPayload {
#NotBlank
private String name;
#NotBlank
private String author;
}
When I call this api without name. It responses like below.
{
"timestamp": "2022-03-26T14:06:43.564+00:00",
"path": "/books",
"status": 400,
"error": "Bad Request",
"requestId": "654248ee-5",
"errors": [
{
"codes": [
"NotBlank.bookPayload.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"bookPayload.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
... omit ...
}
]
}
You can see errors attribute in the response body.
But If I test this api with #SpringBootTest or #WebfluxTest, There is no errors attribute.
#Slf4j
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CommonErrorResponseTest {
private final WebClient web;
public CommonErrorResponseTest(#LocalServerPort Integer port) {
web = WebClient.create("http://localhost:" + port);
}
#Test
void _400_badRequest_violation() {
BookPayload bookPayload = new BookPayload();
bookPayload.setAuthor("John");
Mono<String> stringMono = web.post().uri("/books")
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.bodyValue(bookPayload)
.exchangeToMono(response -> response.bodyToMono(String.class));
String body = stringMono.block();
log.info("body: {}", body);
}
}
console
body: {"timestamp":"2022-03-26T14:19:21.981+00:00","path":"/books","status":400,"error":"Bad Request","requestId":"68df2a79-1"}
I'd like to know why I'm getting different results.
Spring Boot’s DevTools enables the inclusion of binding errors in the error response to ease problem solving during development. You can configure the same behaviour in your tests by setting server.error.include-binding-errors to always.
You can see a complete list of the properties that DevTools sets in the reference documentation.

#Valid | response very verbose, how to customize?

I have a Spring Boot microservice and I want to valide the incoming requestBody of an endpoint.
By using #Valid with #NotBlank I have noticed that the answer is very verbose and my customized error message is deep into the object; here is an example:
{
"timestamp": "2020-12-17T09:28:26.529+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.createUserRequest.username",
"NotBlank.username",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"createUserRequest.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
}
],
"defaultMessage": "USERNAME IS REQUIRED",
"objectName": "createUserRequest",
"field": "username",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='createUserRequest'. Error count: 1",
"path": "/api/user/create"
}
How can I customize this object returned? I would like the response to simply be something like this:
{
"timestamp": "2020-12-17T09:28:26.529+0000",
"status": 400,
"error": "Bad Request",
"message": "USERNAME IS REQUIRED"
}
Here is my code:
Request
#Data
public class CreateUserRequest {
#NotBlank(message = "username is required")
private String username;
#Size(min = 3, max = 64)
#NotBlank(message = "password is required")
private String password;
#NotBlank(message = "confirmPassword is required")
#Size(min = 3, max = 64)
private String confirmPassword;
}
Controller
#PostMapping("/create")
public ResponseEntity<User> createUser(#Valid #RequestBody CreateUserRequest request) {
User user = appService.createUserAndCart(request);
return ResponseEntity.ok(user);
}
Thank you for your experience
You can use #ControllerAdvice/#RestControllerAdvice
it allows you to handle exceptions across the whole application. You can think of it as an interceptor of exceptions thrown by methods annotated with #RequestMapping and similar.
And add a method like this,
#ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleException(Exception ex)
//your custom body
return new ResponseEntity<>(body, HttpStatus.XXXXX);
}
You can specify a specific Exception type (I think it's InvalidArgumentException in your case)
Define a return class
import org.springframework.http.HttpStatus;
import java.util.HashMap;
/**
* #description:
* #author: 582895699#qq.com
* #time: 2020/12/20 下午 01:50
*/
public class Resp extends HashMap {
private static final long serialVersionUID = 1L;
public static final String TIMESTAMP = "timestamp";
public static final String STATUS = "status";
public static final String ERROR = "error";
public static final String MESSAGE = "message";
public static Resp fail(String message) {
Resp resp = new Resp();
resp.put(TIMESTAMP, System.currentTimeMillis());
resp.put(STATUS, HttpStatus.BAD_REQUEST.value());
resp.put(ERROR, HttpStatus.BAD_REQUEST.getReasonPhrase());
resp.put(MESSAGE, message);
return resp;
}
#Override
public Object put(Object key, Object value) {
return super.put(key, value);
}
}
Define global exception handling class and obtain exception information
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* #description:
* #author: 582895699#qq.com
* #time: 2020/12/20 下午 01:55
*/
#RestControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(value = MethodArgumentNotValidException.class)
public Resp methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
ObjectError objectError = bindingResult.getAllErrors().get(0);
String message = objectError.getDefaultMessage();
return Resp.fail(message);
}
}

How to return a custom response pojo when request body fails validations that are defined using Bean Validation/Hibernate Validator?

Is it possible to override the default response POJO of Spring Hibernate validator?
Currently when a validation gets failed then a very big response returned to the client as shown below. But I don't want the client to provide the full error response of hibernate validator and instead to send some key-value pair regarding the error message.
{
"timestamp": "2018-05-28T18:12:56.705+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.abc.xyz",
"NotBlank.xyz",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"abc.xyz",
"xyz"
],
"arguments": null,
"defaultMessage": "transactionId",
"code": "transactionId"
}
],
"defaultMessage": "xyz is mandatory parameter , please provide appropriate value",
"objectName": "abc",
"field": "xyz",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='xyz'. Error count: 1",
"path": "/path/create/1"
}
A BindException is thrown when the request body fails validation. You can define your own ControllerAdvice that construct an appropriate error message from the details in the BindException.
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.example.demo.ErrorResponse.ErrorDetails;
#ControllerAdvice
public class CustomExceptionHandler {
#ExceptionHandler(BindException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ErrorResponse handleException(BindException ex) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors();
List<ErrorDetails> errorDetails = new ArrayList<>();
for (FieldError fieldError : errors) {
ErrorDetails error = new ErrorDetails();
error.setFieldName(fieldError.getField());
error.setMessage(fieldError.getDefaultMessage());
errorDetails.add(error);
}
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setErrors(errorDetails);
return errorResponse;
}
}
Pojo for the error response:
#Data
public class ErrorResponse {
private List<ErrorDetails> errors;
#Data
public static class ErrorDetails {
private String fieldName;
private String message;
}
}
Sample error message
{
"errors": [
{
"fieldName": "firstName",
"message": "must not be null"
},
{
"fieldName": "lastName",
"message": "must not be null"
}
]
}

Springfox Java Bean Validations not displaying in Swagger Output

I'm trying to follow the documentation for Springfox Swagger to get Java Bean Validation to work (http://springfox.github.io/springfox/docs/current/#springfox-support-for-jsr-303), but they are not showing up in the Swagger UI.
This is my Spring Configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
#EnableSwagger2
#Import({springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration.class})
#Configuration
public class SwaggerConfig {
#Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("My API")
.build();
}
}
This is my request mapping:
#ApiOperation(value = "Use to get token for internal applications")
#PostMapping(value = AuthUris.TOKEN)
public AuthResponse token(#Valid #RequestBody AuthRequest authRequest) {
// implementation omitted
}
This is my POJO:
#ApiModel
public class AuthRequest {
#ApiModelProperty
#NotNull
#Size(min = 4, max = 50)
private String username;
#NotNull
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
I expected the NotNull and Size annotations to be captured in the Swagger UI but they are not. Please help me understand how this should work. Thank you.
.
So thanks to https://stackoverflow.com/users/8012379/indra-basak I do see that they were working. However, I have to hover over the field to thinks like #Size. See the screenshot below.
If you are using springfox version 2.7.0, both #NotNull and #Size annotations should work.
Your #NotNull annotation is already working in the password field.
If #ApiModelProperty annotation is present for a field, it takes precedence over #NotNull annotation. It is the case with the username field. It shows up as optional because the required attribute of #ApiModelProperty annotation is set to false by default.
If you use springfox version 2.7.0 and don't use #ApiModelProperty annotation, the model will show up as:
Validation
For example, if you enter a username less than the minimum size of 4, you will get the following exception:
{
"timestamp": 1511550365198,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"errors": [
{
"codes": [
"Size.authRequest.username",
"Size.username",
"Size.java.lang.String",
"Size"
],
"arguments": [
{
"codes": [
"authRequest.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
},
50,
4
],
"defaultMessage": "size must be between 4 and 50",
"objectName": "authRequest",
"field": "username",
"rejectedValue": "s",
"bindingFailure": false,
"code": "Size"
}
],
"message": "Validation failed for object='authRequest'. Error count: 1",
"path": "/tokens"
}

How to replace implementation of javax.validation.Valid annotation in Spring?

Suppose I don't wanna use JSR 303 to validate my beans. Is it possible to implement custom validator which will be used by Spring when bean is marked as #Valid?
It would be great if my custom validator will be a Spring Component
#fedor.belov
I detailed this problem in another question. In my needs I want to keep JSR-303 and make custom validator works, but you can change my example code to fit your needs.
This problem can be solved extending the LocalValidatorFactoryBean, you can override the validate method inside this class giving any behavior that you want.
In my case I need to use JSR-303 AND custom validators for same model in different methods in same Controller, normally is recommended to use #InitBinder, but it is not sufficient for my case because InitBinder make a bind between Model and Validator (if you use #RequestBody InitBinder is just for one model and one validator per Controller).
Controller
#RestController
public class LoginController {
#PostMapping("/test")
public Test test(#Validated(TestValidator.class) #RequestBody Test test) {
return test;
}
#PostMapping("/test2")
public Test test2(#Validated #RequestBody Test test) {
return test;
}
}
Custom Validator
public class TestValidator implements org.springframework.validation.Validator {
#Override
public boolean supports(Class<?> clazz) {
return Test.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Test test = (Test) target;
errors.rejectValue("field3", "weird");
System.out.println(test.getField1());
System.out.println(test.getField2());
System.out.println(test.getField3());
}
}
Class to be validate
public class Test {
#Size(min = 3)
private String field2;
#NotNull
#NotEmpty
private String field1;
#NotNull
#Past
private LocalDateTime field3;
//...
//getter/setter
//...
}
CustomLocalValidatorFactoryBean
public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
private Logger logger = LoggerFactory.getLogger(this.getClass());
#Override
public void validate(#Nullable Object target, Errors errors, #Nullable Object... validationHints) {
Set<Validator> concreteValidators = new LinkedHashSet<>();
Set<Class<?>> interfaceGroups = new LinkedHashSet<>();
extractConcreteValidatorsAndInterfaceGroups(concreteValidators, interfaceGroups, validationHints);
proccessConcreteValidators(target, errors, concreteValidators);
processConstraintViolations(super.validate(target, interfaceGroups.toArray(new Class<?>[interfaceGroups.size()])), errors);
}
private void proccessConcreteValidators(Object target, Errors errors, Set<Validator> concreteValidators) {
for (Validator validator : concreteValidators) {
validator.validate(target, errors);
}
}
private void extractConcreteValidatorsAndInterfaceGroups(Set<Validator> concreteValidators, Set<Class<?>> groups, Object... validationHints) {
if (validationHints != null) {
for (Object hint : validationHints) {
if (hint instanceof Class) {
if (((Class<?>) hint).isInterface()) {
groups.add((Class<?>) hint);
} else {
Optional<Validator> validatorOptional = getValidatorFromGenericClass(hint);
if (validatorOptional.isPresent()) {
concreteValidators.add(validatorOptional.get());
}
}
}
}
}
}
#SuppressWarnings("unchecked")
private Optional<Validator> getValidatorFromGenericClass(Object hint) {
try {
Class<Validator> clazz = (Class<Validator>) Class.forName(((Class<?>) hint).getName());
return Optional.of(clazz.newInstance());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
logger.info("There is a problem with the class that you passed to "
+ " #Validated annotation in the controller, we tried to "
+ " cast to org.springframework.validation.Validator and we cant do this");
}
return Optional.empty();
}
}
Configure application
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public javax.validation.Validator localValidatorFactoryBean() {
return new CustomLocalValidatorFactoryBean();
}
}
Input to /test endpoint:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
Output from /test endpoint:
{
"timestamp": "2018-04-16T17:34:28.532+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"weird.test.field3",
"weird.field3",
"weird.java.time.LocalDateTime",
"weird"
],
"arguments": null,
"defaultMessage": null,
"objectName": "test",
"field": "field3",
"rejectedValue": "2018-04-15T15:10:24",
"bindingFailure": false,
"code": "weird"
},
{
"codes": [
"NotEmpty.test.field1",
"NotEmpty.field1",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"test.field1",
"field1"
],
"arguments": null,
"defaultMessage": "field1",
"code": "field1"
}
],
"defaultMessage": "Não pode estar vazio",
"objectName": "test",
"field": "field1",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='test'. Error count: 2",
"path": "/user/test"
}
Input to /test2 endpoint:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
Output to /test2 endpoint:
{
"timestamp": "2018-04-16T17:37:30.889+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotEmpty.test.field1",
"NotEmpty.field1",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"test.field1",
"field1"
],
"arguments": null,
"defaultMessage": "field1",
"code": "field1"
}
],
"defaultMessage": "Não pode estar vazio",
"objectName": "test",
"field": "field1",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='test'. Error count: 1",
"path": "/user/test2"
}
Original answer with original question.
I hope this help.
You will have to implement Validator interface and mark the implementation as #Component. Then you can bind your custom validator using #InitBinder annotation. Something like this:
#Component
public class MyCustomValidator implements Validator
{
#Override
public boolean supports(Class<?> clazz)
{
// your custom logic
}
#Override
public void validate(Object target, Errors errors)
{
// your custom logic
}
}
To bind it with annotation:
#Controller
public class MyController
{
#Autowired
private MyCustomValidator validator;
#InitBinder
protected void initBinder(final WebDataBinder binder)
{
binder.addValidators(validator);
}
}

Resources