Search code examples
spring-bootspring-restcontrollerhibernate-validatorcontroller-advicejsr380

spring/hibernate validation -> error message is not passed to caller?


I am using org.springframework.boot:spring-boot-starter-validation:2.7.0(which in turn uses hibernate validator) to validate user input to rest controller.
I am using Spring Boot Web Starter (2.7.0) based project with @RestController annotation My @GetMapping method is something like below -

@GetMapping(path = "/abcservice")
public Object abcService(
            @RequestParam(value = "accountId", required = true) String accountId,
            @Valid @RequestParam(value = "offset", required = false, defaultValue = "0") int offset,
            @Valid @RequestParam(value = "limit", required = false, defaultValue = "10000") int limit
    ) throws Exception {

My problem is - I want the user to know about any input validation errors so they can correct and retry. But the framework is just giving 400 status code with below message.

{
    "timestamp": "2022-08-03T16:10:14.554+00:00",
    "status": 400,
    "error": "Bad Request",
    "path": "/somepath/abcservice"
}

On the server side the request is logged in warn.

2022-08-03 21:40:14.535 WARN 98866 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "0s"]

I want this above error message --> Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "0s" also to be passed on to user. Is there a easy configuration based way to achieve.
I think i can add a ControllerAdvice to handle this exception and include this message in the response from handler method. But this will be a couple of lines of code. Is there an even simpler way than the ControllerAdvice approach.

Similarly if the client don't pass the mandatory accountId param, the client is just getting the same 400 response as above. No details or hints to the client about what wrong they are doing or how they can fix it.. but on the server side i can see below warn log.

2022-08-03 21:59:20.195 WARN 235 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'accountId' for method parameter type String is not present]

I want the client to know about this error/exception. Nothing secret here to hide (atleast in my case).

Edit - found this config -

server.error.include-message=always

Now the issue is, bad requests are sent with 500 status code, I want them to sent as 400. Then this Q is solved.
Validations made by @Valid return with 500 Status Code. Is there anyway to tell the server to return 400 response when validations fail (without using ControllerAdvice).

If you wish to test-- you can try -->
Annotate controller with @Validated.
And execute below method and you will see 500 error but would want this to be 400.

@GetMapping("/test")
public void test(@Valid @RequestParam(value = "studentId", required = false)
                         @Min(value=0, message="Can not be less than 0") @Max(value=200, message="Can not be above 200") Long studentId ) {

        System.out.println("hit: ");
    }

And hit - http://localhost:9099/test?studentId=400


Solution

  • The spring in-built solution without global exception handler and with minimal config is by adding the below property in the application.properties.

    server.error.include-binding-errors=always
    

    The above property can have three values:

    1. always ----> All api's in the app will always return well defined validation error message response.
    2. on-param ----> All api's in the app will conditionally return well defined validation error message response based on input request param field "errors"
    3. never ---> to disable this feature.

    Example Github Project Reference

    Demo Test:
    
    package com.example.demo;
    
    import javax.validation.Valid;
    import javax.validation.constraints.NotNull;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    @SpringBootApplication
    @RestController
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
        @PostMapping("/test")
        public void test(@Valid @RequestBody Student student) {
            System.out.println("studentData: " + student);
        }
    
    }
    
    class Student {
    
        @NotNull(message = "firstName cannot be null")
        private String firstName;
        private String lastName;
    
        public String getFirstName() {
            return firstName;
        }
    
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    
        @Override
        public String toString() {
            return "Student [firstName=" + firstName + ", lastName=" + lastName + "]";
        }
    
    }
    

    Request:

    {
        "firstName": null,
        "lastName" : "sai"
    }
    

    Response: (with HTTP response code = 400)

    {
        "timestamp": "2022-08-04T05:23:58.837+00:00",
        "status": 400,
        "error": "Bad Request",
        "errors": [
            {
                "codes": [
                    "NotNull.student.firstName",
                    "NotNull.firstName",
                    "NotNull.java.lang.String",
                    "NotNull"
                ],
                "arguments": [
                    {
                        "codes": [
                            "student.firstName",
                            "firstName"
                        ],
                        "arguments": null,
                        "defaultMessage": "firstName",
                        "code": "firstName"
                    }
                ],
                "defaultMessage": "firstName cannot be null",
                "objectName": "student",
                "field": "firstName",
                "rejectedValue": null,
                "bindingFailure": false,
                "code": "NotNull"
            }
        ],
        "path": "/test"
    }