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

1.1k views Asked by At

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

2

There are 2 answers

6
Arun Sai Mustyala On BEST ANSWER

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"
}
1
abhishek abhi On

Use @expection handler and controller advice this help to handle your issue