java 57 lines · 8 steps

Custom cross-field validation in Spring

A class-level Bean Validation constraint confirms two password fields match, sequenced to run only after single-field checks pass.

Explained by highlit
1@Target({ElementType.TYPE})
2@Retention(RetentionPolicy.RUNTIME)
3@Constraint(validatedBy = PasswordsMatchValidator.class)
4public @interface PasswordsMatch {
5 String message() default "{signup.passwords.mismatch}";
6 Class<?>[] groups() default {};
7 Class<? extends Payload>[] payload() default {};
8}
9 
10public class PasswordsMatchValidator implements ConstraintValidator<PasswordsMatch, SignupForm> {
11 @Override
12 public boolean isValid(SignupForm form, ConstraintValidatorContext context) {
13 if (form.getPassword() == null || form.getConfirmPassword() == null) {
14 return true;
15 }
16 boolean matches = form.getPassword().equals(form.getConfirmPassword());
17 if (!matches) {
18 context.disableDefaultConstraintViolation();
19 context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
20 .addPropertyNode("confirmPassword")
21 .addConstraintViolation();
22 }
23 return matches;
24 }
25}
26 
27public interface FieldChecks {}
28public interface CrossFieldChecks {}
29 
30@PasswordsMatch(groups = CrossFieldChecks.class)
31@GroupSequence({FieldChecks.class, CrossFieldChecks.class, SignupForm.class})
32public class SignupForm {
33 
34 @NotBlank(groups = FieldChecks.class)
35 @Email(groups = FieldChecks.class)
36 private String email;
37 
38 @NotBlank(groups = FieldChecks.class)
39 @Size(min = 8, max = 72, groups = FieldChecks.class)
40 @Pattern(regexp = ".*\\d.*", message = "{signup.password.digit}", groups = FieldChecks.class)
41 private String password;
42 
43 @NotBlank(groups = FieldChecks.class)
44 private String confirmPassword;
45 
46 @AssertTrue(message = "{signup.terms.required}", groups = FieldChecks.class)
47 private boolean acceptedTerms;
48 
49 public String getEmail() { return email; }
50 public void setEmail(String email) { this.email = email; }
51 public String getPassword() { return password; }
52 public void setPassword(String password) { this.password = password; }
53 public String getConfirmPassword() { return confirmPassword; }
54 public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; }
55 public boolean isAcceptedTerms() { return acceptedTerms; }
56 public void setAcceptedTerms(boolean acceptedTerms) { this.acceptedTerms = acceptedTerms; }
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Class-level constraints let you validate relationships between fields that per-field annotations cannot express.
  2. 2Group sequences stagger validation so cheap field checks gate expensive or cross-field rules.
  3. 3Attaching a violation to a specific property node routes the error message to the right form field.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Custom cross-field validation in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code