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

java
@Component
@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {
 

Transparent column encryption in Spring & JPA

encryption aes-gcm jpa-converter
Advanced 10 steps
java
package com.acme.billing.config;
 
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;

Feature-flagged beans with Spring @ConditionalOnProperty

feature-flags conditional-beans strategy-pattern
Intermediate 5 steps
java
public static Map<String, String> parseCookieHeader(String header) {
    Map<String, String> cookies = new LinkedHashMap<>();
    if (header == null || header.isBlank()) {
        return cookies;

Parsing an HTTP Cookie header in Java

string-parsing http url-decoding
Intermediate 6 steps
java
public class TimedSocketReader {
 
    private static final int READ_TIMEOUT_MS = 5_000;
    private static final int CONNECT_TIMEOUT_MS = 3_000;

Reading a socket with connect and read timeouts

sockets timeouts io
Intermediate 8 steps
java
public final class EncodingDetector {
 
    public enum Encoding {
        UTF_8, UTF_16LE, UTF_16BE, UTF_32LE, UTF_32BE, ASCII, UNKNOWN

Detecting text encoding from raw bytes in Java

byte-manipulation encoding-detection bitwise-operations
Intermediate 8 steps
java
public final class ImportOrganizer {
 
    private static final Pattern IMPORT_LINE =
            Pattern.compile("^import\\s+(static\\s+)?([\\w.]+(?:\\.\\*)?)\\s*;\\s*$");

Sorting Java imports with a regex pass

regex sorting text-processing
Intermediate 8 steps

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