How to intercept the error message when spring boot web binding? - java

I try to make user registration codes with spring boot web starter.
First, these are registration form class including constraints.
#Data
public class RegisterForm {
#NotBlank(message = "Not valid username.") // (1)
#Size(min=2, max=30, message="minimum 2 and maximum 30") // (3)
private String username;
#NotBlank(message = "Not valid password") // (1)
#Size(min=5, message = "minimum 5") // (3)
private String password;
#Size(max=50, message="maximum 50") // (3)
private String fullname;
#NotEmpty(message = "Not valid email") // (2)
private String email;
}
And next codes are controller classes which bind User class and registration form class.
#RequestMapping(value="/users/register", method=RequestMethod.POST)
public String register(#Valid RegisterForm registerForm, Model model, BindingResult bindingResult) {
if(!bindingResult.hasErrors()) {
User user = new User();
user.setUsername(registerForm.getUsername());
user.setPassword(registerForm.getPassword());
user.setEmail(registerForm.getEmail());
user.setFullname(registerForm.getFullname());
user.setRole(UserRole.USER);
this.userService.register(user);
return "redirect:/home";
}
return "/users/register";
}
And Below codes are Error Controller class.
#RequestMapping("/error")
public String errorHandle(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if(status != null) {
Integer statusCode = Integer.valueOf(status.toString());
if(statusCode.equals(HttpStatus.BAD_REQUEST.value())) {
return "/errors/400";
} else if(statusCode.equals(HttpStatus.NOT_FOUND.value())) {
return "/errors/404";
} else if(statusCode.equals(HttpStatus.FORBIDDEN.value())) {
return "/errors/403";
} else if(statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR.value())) {
return "/errors/500";
}
}
return "errors/default";
}
And I make the error intentionally ,and then the error message are brought on the console like below and 400 exception is thrown with /error/400 html.
Field error in object 'registerForm' on field 'username': rejected value []; default message [minimum 2 and maximum 30]
Field error in object 'registerForm' on field 'username': rejected value []; default message [Not valid username]
Field error in object 'registerForm' on field 'email': rejected value []; default message [Not valid email]
Field error in object 'registerForm' on field 'password': rejected value []; default message [Not valid password]
Field error in object 'registerForm' on field 'password': rejected value [];default message [minimum 5]]
My issue is I have no idea how to send the field error of registerForm messages to /error/400 html so the user can confirm which field of registerForm violates the constraint. I want to know how field error of registerForm can be transferred to /error/400 html. Any idea, please.

first step:
validate data in the controller like this
#RequestMapping(value="/users/register", method=RequestMethod.POST)
public String register(#Valid RegisterForm registerForm)......
sencond step:
make a controller advice which catch the exceptions
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(value = MethodArgumentNotValidException.class)
#ResponseBody
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
//here you can use api of MethodArgumentNotValidException to do anything you want
//e.getBindingResult(),e.getFieldErrors(),etc;
// you can change the return type of Object
}

You need to extends ResponseEntityExceptionHandler which provide centralized exception handling across all RequestMapping methods. This base class provides an ExceptionHandler method for handling internal Spring MVC exceptions. This method returns a ResponseEntity for writing to the response with a HttpMessageConverter message converter.
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
// All Exceptions Handler.
#ExceptionHandler(Exception.class)
public final ResponseEntity<ExceptionBean> handleAllExceptions(Exception ex, WebRequest request) {...}
// Unique Constraint Violation Exception Handler.
#ExceptionHandler(DataIntegrityViolationException.class)
public final ResponseEntity<ExceptionBean> handleDataIntegrityViolationExceptions(DataIntegrityViolationException ex, WebRequest request) {...}
// Custom Exceptions Handler.
#ExceptionHandler(DomainException.class)
public final ResponseEntity<ExceptionBean> handleDomainExceptions(DomainException ex, WebRequest request) {
ExceptionBean exceptionBean = new ExceptionBean(new Date(), ex.getMessage(),
request.getDescription(false));
LOGGER.error(ex.getMessage(), ex);
return new ResponseEntity<>(exceptionBean, HttpStatus.BAD_REQUEST);
}
}

Related

Spring Boot Validation - How to retrieve input message in MethodArgumentNotValidException handler

I am using spring-boot-starter-validation dependency to validate java beans for my application
My Controller code as below
#PostMapping("/testMessage")
ResponseEntity<String> testMethod(#Valid #RequestBody InternalMsg internalMsg) {
return ResponseEntity.ok("Valid Message");
}
InternalMsg class
public class InternalMsg implements Serializable {
#NotNull(message = "Msg Num is a required field")
private String msgNumber;
#NotEmpty(message = "Activity Name is a required field")
private String activityName;
}
InternalMsg Sample Input JSON
{
"MSGNUMBER": "12345",
"ACTIVITYNAME": "",
}
My handler code as below
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<InternalMsgResponse> handleValidationExceptions(
MethodArgumentNotValidException ex) {
InternalMsgResponse msg = new InternalMsgResponse();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String errorMessage = error.getDefaultMessage(); // Error Message
ack.setStatusNotes(errorMessage);
});
return ResponseEntity.ok().body(msg);
}
As per the sample input, the activity name is empty so I get a validation error message as "Activity Name is a required field".
For some other processing, I need to get the values of input Msg inside handler i.e I want to get msgNumber value(12345) which is a VALID value.
Is it possible? If so, please guide how to retrieve those values inside the handler.
You can call
ex.getTarget()
That will return the object. Then simply cast it to InternalMsg
If you are using an older Spring Boot Version before 2.4.x then you must call
ex.getBindingResult().getTarget()

Return response code as 400 when #notnull validation fails

I'm using javax and Hibernate implementation for validation of my request payload.
version - org.hibernate:hibernate-validator:5.4.1.final
Sample Pojo:
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
public class Customer implements Serializable{
#NotNull(message="name is null")
#Size(min = 1)
public String name;
#NotNull(message="country is null")
#Size(min = 1)
public String country;
}
Sample controller:
#RequestMapping(value = {"/customer"}, method = RequestMethod.POST)
#ResponseBody
public Customer doAdd(#RequestBody #Valid Customer inData){
//some logic
return outData
}
Sample input json:
{
"country":"canada"
}
Console Exception:
org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument at index 0 in method: ........ model.request.Customer,java.lang.String,java.lang.String,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse), with 1 error(s): [Field error in object 'Customer' on field 'name': rejected value [null]; codes [NotNull.Customer.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [Customer.name,name]; arguments []; default message [name]]; default message [name is null]]
Here when customer.name is null, i'm getting response code as 422(Unprocessable entity). But i want to return 400(BAD Request). How can i override the response code here? Any document reference would be appreciated.
note - i don't want to do these validation at controller's side, where can i can check and send the response code accordingly.
Like this one - How to return 400 http status code with #NotNull?
Update - issue was resolved -------
The issue is resolved for me. Thanks all for your response.
Let me explain what was causing this,
HttpRequest ----> #Valid on RequestBody ----> Javax validation on Request object-----> If any of the validation fails, **MethodArgumentNotValidException** exception is thrown.
It is the responsibility of the developer to catch this exception and throw corresponding http response code.
In my case, the exception handler was already there and it was catching this MethodArgumentNotValidException and returning HttpStatus.UNPROCESSABLE_ENTITY. Thats the reason i was seeing 422 error code.
Now i have changed the exception handler as below,
#ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResource>
handleMethodArgumentNotValidException(MethodArgumentNotValidException exception, HttpServletRequest request) {
List<FieldErrorResource> fieldErrorResources = new ArrayList<>();
BindingResult bindingResult = exception.getBindingResult();
for (FieldError constraintViolation : bindingResult.getFieldErrors()) {
fieldErrorResources.add(FieldErrorResource.builder()
.field(constraintViolation.getField())
.resource(request.getContextPath())
.message(constraintViolation.getDefaultMessage()).build());
}
return responseEntityFor(HttpStatus.BAD_REQUEST,
"The content you've sent contains " + bindingResult.getErrorCount() + " validation errors.", fieldErrorResources);
}
You should set BindingResult immediately after your Customer. Like:
#RequestMapping(value = {"/customer"}, method = RequestMethod.POST)
#ResponseBody
public Customer doAdd(#RequestBody #Valid Customer inData, BindingResult bindingResult){
//some logic
return outData
}
You can use RestControllerAdvice for the same
#RestControllerAdvice
public class ExceptionRestControllerAdvice {
#ExceptionHandler({ConstraintViolationException.class})
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ExceptionResponseMessage handleInvalidParameterException(RuntimeException ex) {
return sendResponse(HttpStatus.BAD_REQUEST, ex);
}
private ExceptionResponseMessage sendResponse(HttpStatus status, RuntimeException ex) {
return new ExceptionResponseMessage(Instant.now(), status.value(), status.getReasonPhrase(),
ex.getClass().toString(), ex.getMessage());
}
}
public class ExceptionResponseMessage {
private Instant time;
private int status;
private String error;
private String exception;
private String message;
// setter and getter and constructor

#NotNull : validation custom message not displaying

I am using spring data JPA for creating application. In that I am trying to implement server side validation using annotation. I added #NotNull annotation on filed with custom message. I also added #valid with #RequestBody
But problem is that when I am passing nAccountId as null I am not getting custom message i.e. Account id can not be null I am getting "message": "Validation failed for object='accountMaintenanceSave'. Error count: 1",.
Can any one please tell me why I am not getting custom message?
Controller code
#PutMapping("/updateAccountData")
public ResponseEntity<Object> saveData(#Valid #RequestBody AccountMaintenanceSave saveObj){
return accService.saveData(saveObj);
}
AccountMaintenanceSave class
import javax.validation.constraints.NotNull;
public class AccountMaintenanceSave {
#NotNull(message="Account id can not be null")
public Integer nAccountId;
#NotNull
public String sClientAcctId;
#NotNull
public String sAcctDesc;
#NotNull
public String sLocation;
#NotNull
public Integer nDeptId;
#NotNull
public Integer nAccountCPCMappingid;
#NotNull
public Integer nInvestigatorId;
//Getter and Setter
}
RestExceptionHandler class
#ControllerAdvice
public class RestExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleAllExceptionMethod(Exception ex, WebRequest requset) {
ExceptionMessage exceptionMessageObj = new ExceptionMessage();
exceptionMessageObj.setMessage(ex.getLocalizedMessage());
exceptionMessageObj.setError(ex.getClass().getCanonicalName());
exceptionMessageObj.setPath(((ServletWebRequest) requset).getRequest().getServletPath());
// return exceptionMessageObj;
return new ResponseEntity<>(exceptionMessageObj, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
I don't know what exactly happen previously and not getting proper message. Now using same code getting result like this with proper message
{
"message": "Validation failed for argument at index 0 in method: public org.springframework.http.ResponseEntity<java.lang.Object> com.spacestudy.controller.AccountController.saveData(com.spacestudy.model.AccountMaintenanceSave), with 1 error(s): [Field error in object 'accountMaintenanceSave' on field 'nAccountId': rejected value [null]; codes [NotNull.accountMaintenanceSave.nAccountId,NotNull.nAccountId,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [accountMaintenanceSave.nAccountId,nAccountId]; arguments []; default message [nAccountId]]; default message [Account id can not be null]] ",
"error": "org.springframework.web.bind.MethodArgumentNotValidException",
"path": "/spacestudy/rockefeller/admin/account/updateAccountData"
}
In message filed can I print only [Account id can not be null]?
Try this.
#ControllerAdvice
public class RestExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleAllExceptionMethod(Exception ex, WebRequest requset) {
ExceptionMessage exceptionMessageObj = new ExceptionMessage();
// Handle All Field Validation Errors
if(ex instanceof MethodArgumentNotValidException) {
StringBuilder sb = new StringBuilder();
List<FieldError> fieldErrors = ((MethodArgumentNotValidException) ex).getBindingResult().getFieldErrors();
for(FieldError fieldError: fieldErrors){
sb.append(fieldError.getDefaultMessage());
sb.append(";");
}
exceptionMessageObj.setMessage(sb.toString());
}else{
exceptionMessageObj.setMessage(ex.getLocalizedMessage());
}
exceptionMessageObj.setError(ex.getClass().getCanonicalName());
exceptionMessageObj.setPath(((ServletWebRequest) requset).getRequest().getServletPath());
// return exceptionMessageObj;
return new ResponseEntity<>(exceptionMessageObj, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
It's not so good to make your only ExceptionHandler to catch Exception.class make it ConstraintViolationException.class
Another approach to the solution:
I would suggest remove the exception handler for this validation fields in the POJO and rather let Spring handle the error response by adding the below property into application.properties. By adding this the message configured in the #notnull or any validation annotation can be captured and showed in the response by default and no explicit handling of this validation case is required.
server.error.include-message=always
server.error.include-binding-errors=always

Spring Boot Validate JSON Mapped via ObjectMapper GET #RequestParam

What's the simplest approach to validating a complex JSON object being passed into a GET REST contoller in spring boot that I am mapping with com.fasterxml.jackson.databind.ObjectMapper?
Here is the controller:
#RestController
#RequestMapping("/products")
public class ProductsController {
#GetMapping
public ProductResponse getProducts(
#RequestParam(value = "params") String requestItem
) throws IOException {
final ProductRequest productRequest =
new ObjectMapper()
.readValue(requestItem, ProductRequest.class);
return productRetriever.getProductEarliestAvailabilities(productRequest);
}}
DTO request object I want to validate:
public class ProductRequest {
private String productId;
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}}
I was thinking of using annotations on the request DTO however when I do so, they are not triggering any type of exceptions, i.e. #NotNull. I've tried various combinations of using #Validated at the controller as well as #Valid in the #RequestParam and nothing is causing the validations to trigger.
In my point of view, Hibernate Bean Validator is probably one of the most convenient methods to validate the annotated fields of a bean anytime and anywhere. It's like setup and forget
Setup the Hibernate Bean Validator
Configure how the validation should be done
Trigger the validator on a bean anywhere
I followed the instructions in the documentation given here
Setup dependencies
I use Gradle so, I am going to add the required dependencies as shown below
// Hibernate Bean validator
compile('org.hibernate:hibernate-validator:5.2.4.Final')
Create a generic bean valdiator
I setup a bean validator interface as described in the documentation and then use this to validate everything that is annotated
public interface CustomBeanValidator {
/**
* Validate all annotated fields of a DTO object and collect all the validation and then throw them all at once.
*
* #param object
*/
public <T> void validateFields(T object);
}
Implement the above interface as follow
#Component
public class CustomBeanValidatorImpl implements CustomBeanValidator {
ValidatorFactory valdiatorFactory = null;
public CustomBeanValidatorImpl() {
valdiatorFactory = Validation.buildDefaultValidatorFactory();
}
#Override
public <T> void validateFields(T object) throws ValidationsFatalException {
Validator validator = valdiatorFactory.getValidator();
Set<ConstraintViolation<T>> failedValidations = validator.validate(object);
if (!failedValidations.isEmpty()) {
List<String> allErrors = failedValidations.stream().map(failure -> failure.getMessage())
.collect(Collectors.toList());
throw new ValidationsFatalException("Validation failure; Invalid request.", allErrors);
}
}
}
The Exception class
The ValidationsFatalException I used above is a custom exception class that extends RuntimeException. As you can see I am passing a message and a list of violations in case the DTO has more than one validation error.
public class ValidationsFatalException extends RuntimeException {
private String message;
private Throwable cause;
private List<String> details;
public ValidationsFatalException(String message, Throwable cause) {
super(message, cause);
}
public ValidationsFatalException(String message, Throwable cause, List<String> details) {
super(message, cause);
this.details = details;
}
public List<String> getDetails() {
return details;
}
}
Simulation of your scenario
In order to test whether this is working or not, I literally used your code to test and here is what I did
Create an endpoint as shown above
Autowire the CustomBeanValidator and trigger it's validateFields method passing the productRequest into it as shown below
Create a ProductRequest class as shown above
I annotated the productId with #NotNull and #Length(min=5, max=10)
I used Postman to make a GET request with a params having a value that is url-encoded json body
Assuming that the CustomBeanValidator is autowired in the controller, trigger the validation as follow after constructing the productRequest object.
beanValidator.validateFields(productRequest);
The above will throw exception if any violations based on annotations used.
How is the exception handled by exception controller?
As mentioned in the title, I use ExceptionController in order to handle the exceptions in my application.
Here is how the skeleton of my exception handler where the ValidationsFatalException maps to and then I update the message and set my desired status code based on exception type and return a custom object (i.e. the json you see below)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler({SomeOtherException.class, ValidationsFatalException.class})
public #ResponseBody Object handleBadRequestExpection(HttpServletRequest req, Exception ex) {
if(ex instanceof CustomBadRequestException)
return new CustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage());
else
return new DetailedCustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage(),((ValidationsFatalException) ex).getDetails());
}
Test 1
Raw params = {"productId":"abc123"}
Url encoded parmas = %7B%22productId%22%3A%22abc123%22%7D
Final URL: http://localhost:8080/app/product?params=%7B%22productId%22%3A%22abc123%22%7D
Result: All good.
Test 2
Raw params = {"productId":"ab"}
Url encoded parmas = %7B%22productId%22%3A%22ab%22%7D
Final URL: http://localhost:8080/app/product?params=%7B%22productId%22%3A%22ab%22%7D
Result:
{
"statusCode": 400,
"status": "BAD_REQUEST",
"message": "Validation failure; Invalid request.",
"details": [
"length must be between 5 and 10"
]
}
You can expand the Validator implementation to provide a mapping of field vs message error message.
Do you mean something like this ?
#RequestMapping("/products")
public ResponseEntity getProducts(
#RequestParam(value = "params") String requestItem) throws IOException {
ProductRequest request = new ObjectMapper().
readValue(requestItem, ProductRequest.class);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<ProductRequest>> violations
= validator.validate(request);
if (!violations.isEmpty()) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok().build();
}
public class ProductRequest {
#NotNull
#Size(min = 3)
private String id;
public String getId() {
return id;
}
public String setId( String id) {
return this.id = id;
}
}

"Invalid property 'ssn' of bean class [java.lang.String]" when trying to validate request body with Spring

I have a springboot application with a rest controller sitting up top. The user access the controller through /test and passes in a json like so:
{"ssn":"123456789"}
I want to validate the input by at least making sure there's not an empty ssn being passed in like so:
{"ssn":""}
So here's my controller:
#RequestMapping(
value = "/test",
method = RequestMethod.POST,
consumes = "application/json",
produces = "application/json")
#ResponseBody
public JsonNode getStuff(#RequestHeader HttpHeaders header,
#RequestBody String payload,
BindingResult bindingResult) {
validator.validate(payload, bindingResult);
if(bindingResult.hasErrors()) {
throw new InvalidRequestException("The request is incorrect", bindingResult);
}
/* doing other stuff */
}
And here's my validator:
#Component
public class RequestValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return false;
}
#Override
public void validate(Object target, Errors errors) {
ObjectMapper mapper = new ObjectMapper();
JsonNode ssn = null;
try {
ssn = mapper.readTree((String) target);
} catch (IOException e) {
e.printStackTrace();
}
if(ssn.path("ssn").isMissingNode() || ssn.path("ssn").asText().isEmpty()) {
errors.rejectValue("ssn", "Missing ssn", new Object[]{"'ssn'"}, "Must provide a valid ssn.");
}
}
}
I tried testing this with postman and I keep getting this error:
HTTP Status 500 - Invalid property 'ssn' of bean class [java.lang.String]: Bean property 'ssn' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
What exactly is the problem here? I don't understand what it's talking about in relation to getters and setters.
Edit 1: The value of the payload as requested
{"ssn":""}
By default Spring Boot configures Json parser, so any Json you pass to the controller will be parsed. Spring is expecting an object with a property called 'ssn' to bind the request value.
This means that you should create a model object like this:
public class Data {
String ssn;
}
And use it to bind your request body like this:
#RequestMapping(
value = "/test",
method = RequestMethod.POST,
consumes = "application/json",
produces = "application/json")
#ResponseBody
public JsonNode getStuff(#RequestHeader HttpHeaders header,
#RequestBody Data payload,
BindingResult bindingResult) {
validator.validate(payload, bindingResult);
if(bindingResult.hasErrors()) {
throw new InvalidRequestException("The request is incorrect", bindingResult);
}
/* doing other stuff */
}
You also need to adapt your Validator to use this new Data object.

Categories

Resources