What You'll Learn

public class Person {

	@NotNull
	@Size(min=1, max=40)
	private String firstName;

	@Pattern(regexp=".+@.+\\.[a-z]+")
	private String email;

	...
}
  ...

  @PostMapping("/")
	public String update(@Valid Person person, BindingResult bindingResult) {

		if (bindingResult.hasErrors()) {
			return "profile";
		}

		return "redirect:/profile-updated";
	}

Errors can be displayed in the returned HTML page using Thymeleaf, for example:

...
<form action="#" th:action="@{/}" th:object="${person}" method="post">
	<table>
			<tr>
					<td>Name:</td>
					<td><input type="text" th:field="*{firstName}" /></td>
					<td th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}">Name Error</td>
			</tr>
			<tr>
					<td>Name:</td>
					<td><input type="text" th:field="*{email}" /></td>
					<td th:if="${#fields.hasErrors('email')}" th:errors="*{email}">Email Error</td>
			</tr>
			 ...
	</table>
</form>
...
public class Person {

	private String firstName;

	private String email;

	private String password;

	...
}
@Documented
@Constraint(validatedBy = PasswordValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Password {


    String message() default "Password does match the password policy";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}
public class PasswordValidator implements ConstraintValidator<Password, String> {

		@Override
		public void initialize(Password paramA) {
		}

    public boolean isValid(String password, ConstraintValidatorContext ctx) {
        if (password == null || password.length() < 9) {
            return false;
        }
				return true;
    }
}

...
 @Password
 private String password;
...
  ...

  @PostMapping("/")
	public String update(@Valid Person person, BindingResult bindingResult) {

		if (bindingResult.hasErrors()) {
			return "profile";
		}

		return "redirect:/profile-updated";
	}