The labs on this site need JavaScript to run. The same content is written out below.
Learn › Spring › Web MVC
Validation
The field has @NotBlank, so why does an empty value still get in?
Input values come with conditions such as "the name must not be empty". Bean Validation is a way of writing such conditions above the fields as annotations like @NotBlank, and a condition written this way is called a constraint. However, a constraint does nothing just by being written down. Something has to say "validate this object", and in Spring MVC, the @Valid on a controller parameter is that signal.
Checked against real execution · Checked by sending requests for 48 combinations to a Spring Boot 4.1.1 server (Hibernate Validator 9.1.3)
Concepts in this lab
- Bean Validation and @Valid
- Bean Validation is a Java standard in which conditions such as "this value must not be empty" are written on fields as annotations like @NotBlank and @Min, and a validator takes an object and checks those conditions. A condition written this way is called a constraint, and the standard defines only the contract, while the actual checking is done by an implementation library such as Hibernate Validator. A constraint does not work just by being written down. In Spring MVC, Spring passes the object to the validator only when the controller parameter is annotated with @Valid (or the Spring annotation @Validated). When an object contains another object as a field, that field also needs @Valid for the constraints of the inner object to be checked.
- @NotNull, @NotEmpty, and @NotBlank
- These are three constraints that express the condition that a value "must be present", and they differ from one another in what counts as "not present". @NotNull blocks only null, @NotEmpty blocks null and a value of length 0 (the empty string ""), and @NotBlank additionally blocks a string made only of whitespace (" "). So a string field that has only @NotNull lets an empty string through. @NotBlank is usually used for text that a person types in. @NotBlank is a constraint for strings, so putting it on a numeric field such as an Integer causes an error at validation time.
- BindingResult
- BindingResult is an object that collects the errors that occur while the values of a request are put into an object and validated. In a controller method, when a BindingResult parameter is placed immediately after the parameter annotated with @Valid, Spring does not throw an exception even if validation fails. It puts the errors in this object and runs the controller as usual. The controller checks whether there are errors with hasErrors() and decides for itself how to respond. If it does not check, the invalid value is handled as it is, so when you receive a BindingResult, you must check it. Its position also matters: if it is placed anywhere other than immediately after the parameter to validate, an error occurs when the request is handled.
- MethodArgumentNotValidException
- MethodArgumentNotValidException is the exception that Spring MVC throws when the object of a controller parameter annotated with @Valid fails validation. When this exception is thrown, the controller method does not run, and unless the exception is handled separately, Spring responds with 400 Bad Request. The exception contains which field violated which constraint, so you can handle it with an @ExceptionHandler and build the body of the error response yourself. When what fails is not an object but a constraint declared directly on a parameter, such as an @RequestParam parameter, a different exception named HandlerMethodValidationException is thrown. The names of the exceptions have changed across Spring versions, so it is safest to check them in the version you use.
- HTTP status codes
- A three-digit number that shows the result of the server's handling of a request. 2xx means success, 3xx means a move to another location (redirection), 4xx means a problem with the client's request, and 5xx means an error on the server side. Common ones are 200 (success), 201 (created), 400 (bad request), 401 (authentication required), 403 (no permission), 404 (not found), and 500 (server error).
Try these
- Default settings — The field has @NotBlank, but an empty name is registered as it is, because the parameter has no @Valid.
- With @Valid — The same request is rejected with 400 before it reaches the controller.
- @NotNull lets an empty string through — An empty string is not null. The empty name is registered even though validation ran.
- A single space passes even @NotEmpty — A space also counts as one character. @NotBlank is the one that also blocks a value made only of whitespace.
- With a BindingResult — Even when validation fails, no exception is thrown and the controller runs.
- A constraint directly on the parameter — It is checked even without @Valid. When it fails, the name of the exception is different.
- Sending no @RequestParam value at all — The request is rejected first as "required value is missing", before the constraint is checked.
Running with the default settings
- The request arrives: POST /items, and the body is {"name": ""}. name was sent as an empty string (a string with no characters).
- @RequestBody ItemForm form: Spring reads the JSON body, creates an ItemForm object, and fills in the name field. The name field is "". This step only copies the value in. The @NotBlank constraint above the field has not done anything yet.
- Deciding whether to validate: Spring passes the object to the validator only when the parameter is annotated with @Valid (or @Validated). This parameter has no @Valid, so validation is skipped. The @NotBlank constraint in ItemForm is only a mark that says "this field must be like this", and it cannot block a value by itself.
- add(form) is called: The controller method runs. The value of form.getName() is "". The text returned is "Registered: []", and Spring responds with 200. A value that violates the @NotBlank constraint has come all the way into the controller with nothing stopping it.
Result of this run
- Putting a constraint on a field is not enough for the field to be validated. Spring calls the validator only when the controller parameter is annotated with @Valid.
Code
// Request sent
// POST /items
// Content-Type: application/json
//
// {"name": ""}
class ItemForm {
@NotBlank
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
@RestController
class ItemController {
@PostMapping("/items")
String add(@RequestBody ItemForm form) {
return "Registered: [" + form.getName() + "]";
}
}
Assumptions of this model
All 48 combinations of the 4 ways of receiving the value, the 3 constraints, and the 4 values sent were sent as HTTP requests to a real Spring Boot 4.1.1 server (Spring Framework 7.0.9, embedded Tomcat 11, Hibernate Validator 9.1.3.Final, jakarta.validation-api 3.1.1, JDK 21), and the status code, whether the validator actually ran, the violated constraint, whether the controller ran and the text it returned, and the name of the exception Spring threw were confirmed to match the screen. Whether the validator ran was confirmed by adding one more constraint that always passes and seeing whether it was called (it is not in the code on the screen). Every URL on the screen is /items, but for the check, each combination had its own URL. Spring Boot sets up a validator automatically when the validation library (spring-boot-starter-validation) is present, and this lab assumes that state. The behavior without the library was not verified. The names of the exceptions and the behavior of a constraint declared directly on a parameter depend on the Spring version. What is written here was observed on Spring Framework 7.0.9, and versions before 6.1 are known to behave differently, but they were not run. The body of an error response and the text of the error messages vary with the configuration and the language of the server, so they were not treated as results. The default error response body seen on this PC contained only timestamp, status, error, and path, and did not say which field was wrong. Cases that are not on the screen were also checked on the same server. Putting the Spring annotation @Validated on the parameter instead of @Valid gives the same results. Receiving form data with @Valid @ModelAttribute gives the same 12 results (3 constraints, 4 values) as @Valid @RequestBody, and the exception is also MethodArgumentNotValidException. Writing "name": null in the JSON is the same as not sending name. When an object contains another object as a field, the constraints of the inner object are checked only if that field has @Valid (without it, the result is 200), and when the inner object is null, validation does not go into it even with @Valid, and it passes. A constraint that specifies groups is not checked with @Valid and is checked only when the parameter is received with @Validated(that group), and conversely, a constraint without groups is not checked when the parameter is received with @Validated(that group). When two fields both violate their constraints, there are 2 errors, so validation does not stop at the first error. When an @Valid object and a parameter with a constraint declared directly on it are received in the same method, a failure on the object side also comes out as HandlerMethodValidationException. When the value is received with @RequestParam(required = false) and is not sent, null comes in and is caught by @NotNull and @NotBlank. Putting @Validated on the controller class changes the failure of a parameter constraint into ConstraintViolationException and 500. Putting @NotBlank on an Integer field gives 500 with UnexpectedTypeException at validation time. Placing the BindingResult before the @Valid parameter gives 500 with IllegalStateException. If you handle MethodArgumentNotValidException with an @ExceptionHandler, you can get which field violated which constraint and write it in the body of the 400 response yourself. The screen is simplified to one field and one constraint, and for whitespace, only a single space character was checked.
Previous lab: Request mapping and parameter binding · Next lab: REST APIs and message converters