Validate phone numbers in Spring Boot
Phone number validation in Spring Boot that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets Spring Boot's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the Java guide; the rest is Spring Boot's own validation hook, a few lines long.
1. The file
Take Phone.java from the Java guide, with the plans of the countries you serve (the link opens it for the United Kingdom, the United States and France, the plans the tests below use). It needs Java 17 or later; standard library only. Its parse reads a number as typed and returns the number with its E.164 form, region and line type, or Optional.empty() when no plan accepts it.
<!-- pom.xml, in a Spring Boot app -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>2. The Spring Boot side
ValidPhoneNumber.java
The constraint annotation.
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** A phone number readable in the plans of Phone.java; national numbers are read in defaultRegion. */
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
String message() default "not a valid phone number";
String defaultRegion() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}PhoneNumberValidator.java
The validator: the language guide's parse.
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
private String defaultRegion;
@Override
public void initialize(ValidPhoneNumber constraint) {
defaultRegion = constraint.defaultRegion().isEmpty() ? null : constraint.defaultRegion();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || Phone.parse(value, defaultRegion).isPresent(); // null is @NotNull's question
}
}Signup.java
A request body that uses it.
import jakarta.validation.constraints.NotBlank;
public record Signup(@NotBlank String name, @NotBlank @ValidPhoneNumber(defaultRegion = "GB") String phone) {
/** What to store: the E.164 form (call after validation). */
public String phoneE164() {
return Phone.parse(phone, "GB").orElseThrow().e164();
}
}3. The tests
SignupTest.java: The tests, with JUnit 5 and the validator Spring Boot uses.
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SignupTest {
static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
@Test
void nationalNumberInTheDefaultRegion() {
Signup signup = new Signup("Ada", "07400 123456");
assertTrue(VALIDATOR.validate(signup).isEmpty());
assertEquals("+447400123456", signup.phoneE164());
}
@Test
void internationalNumber() {
assertTrue(VALIDATOR.validate(new Signup("Ada", "+1 201-555-0123")).isEmpty());
}
@Test
void oneViolationOnThePhone() {
Set<ConstraintViolation<Signup>> violations = VALIDATOR.validate(new Signup("Ada", "+44 7400 12345"));
assertEquals(1, violations.size());
ConstraintViolation<Signup> violation = violations.iterator().next();
assertEquals("phone", violation.getPropertyPath().toString());
assertEquals("not a valid phone number", violation.getMessage());
}
}mvn testRun before publishing with Hibernate Validator 9.0.1 (Jakarta Validation 3.1) and the file generated for GB, US, FR, as 2026-10-25.
What is specific to Spring Boot
- In a controller,
@Valid @RequestBody Signup signupruns the constraint; a violation becomes a 400 with Spring Boot's defaults (MethodArgumentNotValidException). - The annotation targets fields and parameters only. On a record component that puts it on the field and the constructor parameter, so a bean is validated once; adding
METHODwould also constrain the accessor and report each failure twice. - The validator returns true for null so that
@NotNullor@NotBlankdecides whether the field is required, as Bean Validation expects of every constraint. - Phone.java, like the classes here, has no package line: add the one of your project to each file.