Search code examples
javaspringvalidation

How to throw custom exception in proper way when using @javax.validation.Valid?


How to throw a custom exception in the proper way when using @javax.validation.Valid?

I'm using @Valid in controller, and @AssertTrue to validate request body fields.

public ResponseEntity<Foo> createFoo(
    @Valid @RequestBody Foo FooRequest ...
    @AssertTrue()
    public boolean isFooValid() {
        if (invalid)
            return false;
        ...
    }

However, I want to throw customized Exception class in some condition.

    @AssertTrue()
    public boolean isFooValid() {
        if (invalid)
            return false;
        ...

        // note below
        if (invalidInAnotherCondition)
            throw new CustomizedException(...);
    }

I know this is not desirable way to utilize @Valid in controller, and @AssertTrue. Nevertheless, as I can make my own Exception class which contains customized error info, with the convenience of @Valid.

However the error happens.

javax.validation.ValidationException: HV000090: Unable to access isFooValid
    at org.hibernate.validator.internal.util.ReflectionHelper.getValue(ReflectionHelper.java:245)
    at org.hibernate.validator.internal.metadata.location.GetterConstraintLocation.getValue(GetterConstraintLocation.java:89)
    at org.hibernate.validator.internal.engine.ValueContext.getValue(ValueContext.java:235)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:549)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:515)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:485)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:447)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:397)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:173)
    at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:117)
    at org.springframework.boot.autoconfigure.validation.ValidatorAdapter.validate(ValidatorAdapter.java:70)
    at org.springframework.validation.DataBinder.validate(DataBinder.java:889)
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.validateIfApplicable(AbstractMessageConverterMethodArgumentResolver.java:266)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:137)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:888)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:523)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
    at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
    at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
    at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
    at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
    at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
    at io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
    at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)
    at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
    at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
    at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
    at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
    at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:269)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:78)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:133)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:130)
    at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
    at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:249)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:78)
    at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:99)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:376)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.reflect.InvocationTargetException: null
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.hibernate.validator.internal.util.ReflectionHelper.getValue(ReflectionHelper.java:242)
    ... 70 common frames omitted
Caused by: com.finda.services.finda.common.exception.CustomizedException: 'df282e0d-1205-4574-adaa-0af819af66c0' 
    at ...
    ... 75 common frames omitted

I think this happens because originally, @AssertTrue throws its own Exception itself and it is to be processed through the internal logic; However, customized thrown Exception is not acceptable which can be seen in Caused by: java.lang.reflect.InvocationTargetException: null and javax.validation.ValidationException: HV000090: Unable to access isFooValid

So my final question is below,

Can I bypass this error, still throwing customized Exception?

I really appreciate that you read this long posting in advance.


Solution

  • Consider the example below where I implemented something like what you asking for:

    @RestController
    @RequestMapping("/accounts")
    public class SavingsAccountController {
    
       private final BankAccountService accountService;
    
       @Autowired
       public SavingsAccountController(SavingsAccountService accountService) {
           this.accountService = accountService;
       }
    
       @PutMapping("withdraw")
       public ResponseEntity<AccountBalance> onMoneyWithdrawal(@RequestBody @Validated WithdrawMoney withdrawal, BindingResult errors) {
    
           //this is the validation barrier
           if (errors.hasErrors()) {
               throw new ValidationException(errors);
           }
    
           double balance = accountService.withdrawMoney(withdrawal);
           return ResponseEntity.ok(new AccountBalance(
                   withdrawal.getAccountNumber(), balance));
       }
    
       @PutMapping("save")
       public ResponseEntity<AccountBalance> onMoneySaving(@RequestBody @Validated SaveMoney savings, BindingResult errors) {
    
           //this is the validation barrier
           if (errors.hasErrors()) {
               throw new ValidationException(errors);
           }
    
           double balance = accountService.saveMoney(savings);
           return ResponseEntity.ok(new AccountBalance(
                   savings.getAccountNumber(), balance));
       }
    }
    

    In the code above, we're using Bean Validation to check that the user's DTO contains valid information. Any errors found in the DTO are provided through the BindingResult errors variable, from where the developer can extract all the details of what went wrong during the validation phase.

    To make it easier for the developers to deal with this pattern, in the code above, I simply wrap the BindingResult into a custom ValidationException which knows how to extract the validation error details.

    public class ValidationException extends RuntimeException {
    
       private final BindingResult errors;
    
       public ValidationException(BindingResult errors) {
           this.errors = errors;
       }
    
       public List<String> getMessages() {
           return getValidationMessage(this.errors);
       }
    
    
       @Override
       public String getMessage() {
           return this.getMessages().toString();
       }
    
    
       //demonstrate how to extract a message from the binging result
       private static List<String> getValidationMessage(BindingResult bindingResult) {
           return bindingResult.getAllErrors()
                   .stream()
                   .map(ValidationException::getValidationMessage)
                   .collect(Collectors.toList());
       }
    
       private static String getValidationMessage(ObjectError error) {
           if (error instanceof FieldError) {
               FieldError fieldError = (FieldError) error;
               String className = fieldError.getObjectName();
               String property = fieldError.getField();
               Object invalidValue = fieldError.getRejectedValue();
               String message = fieldError.getDefaultMessage();
               return String.format("%s.%s %s, but it was %s", className, property, message, invalidValue);
           }
           return String.format("%s: %s", error.getObjectName(), error.getDefaultMessage());
       }
    
    }
    

    Notice that in my controller definition I do not use Bean Validation's @Valid annotation, but the Spring counterpart @Validated, but under the hood Spring will use Bean Validation.

    How to Serialize the Custom Exception?

    In the code above the ValidationException will be thrown when the payload is invalid. How should the controller create a response for the client out of this?

    There are multiple ways to deal with this, but perhaps the simplest solution is to define a class annotated as @ControllerAdvice. In this annotated class we will place our exception handlers for any specific exception that we want to handle and turn them into a valid response object to travel back to our clients:

    @ControllerAdvice
    public class ExceptionHandlers {
    
       @ExceptionHandler
       public ResponseEntity<ErrorModel> handle(ValidationException ex) {
           return ResponseEntity.badRequest()
                                .body(new ErrorModel(ex.getMessages()));
       }
    
       //...
    }
    

    I wrote a few other examples of this and other validation techniques with Spring in case you may be interested in reading more about it.