Validate phone numbers in Rust with the validator crate
Phone number validation in Rust validator that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets Rust validator's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the Rust guide; the rest is Rust validator's own validation hook, a few lines long.
1. The file
Take src/phone.rs from the Rust 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 Rust 1.80 or later; the regex crate. Its parse reads a number as typed and returns the number with its E.164 form, region and line type, or None when no plan accepts it.
cargo add validator --features derive2. The Rust validator side
src/signup.rs
The function and a struct that uses it (the tests sit at the end of the file).
use validator::{Validate, ValidationError};
use crate::phone;
/// A phone number readable in the plans of phone.rs; national numbers are read in the United Kingdom's plan.
pub fn phone_number_gb(text: &str) -> Result<(), ValidationError> {
match phone::parse(text, Some("GB")) {
Some(_) => Ok(()),
None => Err(ValidationError::new("phone_number").with_message("not a valid phone number".into())),
}
}
#[derive(Debug, Validate)]
pub struct Signup {
#[validate(length(min = 1))]
pub name: String,
#[validate(custom(function = "phone_number_gb"))]
pub phone: String,
}3. The tests
src/signup.rs (the end of the file): The tests, with cargo test.
#[cfg(test)]
mod tests {
use super::*;
fn signup(phone: &str) -> Signup {
Signup { name: "Ada".into(), phone: phone.into() }
}
#[test]
fn national_number_in_the_default_region() {
assert!(signup("07400 123456").validate().is_ok());
}
#[test]
fn international_number() {
assert!(signup("+1 201-555-0123").validate().is_ok());
}
#[test]
fn error_on_the_phone_field() {
let errors = signup("+44 7400 12345").validate().unwrap_err();
let on_phone = errors.field_errors().get("phone").copied().expect("an error on phone");
assert_eq!(on_phone[0].code, "phone_number");
}
}cargo testRun before publishing with validator 0.21.0 and the file generated for GB, US, FR, as 2026-10-25.
What is specific to Rust validator
- The derive calls the function with the field borrowed,
&String, which Rust passes as&str; returnOk(())or aValidationErrorwith a code and a message. - The region is in the function, since the attribute names a function, not a call: write one function per region you read national numbers in, or parse in the handler with the user's country.
- Axum and Actix have no built-in validation: call
.validate()on the extracted value, or use a validating extractor, and answer 422 with the errors.