Validation
Larastvel provides a Laravel-inspired validation system with 25 built-in rules.
Basic Usage
use larastvel_core::validation::{validate, rules};
use std::collections::HashMap;
use serde_json::json;
let mut data = HashMap::new();
data.insert("email".to_string(), json!("user@example.com"));
data.insert("name".to_string(), json!("John"));
let result = validate(&data, vec![
("email", vec![rules::required(), rules::email()]),
("name", vec![rules::required(), rules::min(2), rules::max(50)]),
]);
match result {
Ok(()) => { /* valid */ }
Err(errors) => {
// errors.has("email")
// errors.first("email")
// errors.to_json()
}
}Available Rules
| Rule | Description |
|---|---|
required() | Field must be present and non-empty |
email() | Must be a valid email |
min(n) | Minimum string length |
max(n) | Maximum string length |
between(a, b) | Length between a and b |
string() | Must be a string |
numeric() | Must be a number |
boolean() | Must be a boolean |
alpha() | Must contain only letters |
alpha_numeric() | Must contain only letters/numbers |
url() | Must be a valid URL |
active_url() | Must be a valid URL whose host resolves in DNS (Laravel 13.22 parity) |
ip() | Must be a valid IP address |
confirmed() | Field must match field_confirmation |
same(field) | Must match another field |
different(field) | Must differ from another field |
size(n) | Exact length |
present() | Field must exist (can be null/empty) |
prohibited() | Field must be absent |
min_value(n) | Numeric minimum |
max_value(n) | Numeric maximum |
regex(pattern) | Must match regex pattern |
base64() | Must be valid base64 (Laravel 13.21 parity — decodes and re-encodes canonically, so padding is enforced) |
unique(table, column?) | Value must not exist in the given table (DB-backed) |
unique_except(table, column?, id) | Value must not exist, ignoring the row with this id (DB-backed) |
exists(table, column?) | Value must exist in the given table (DB-backed) |
Database-Backed Rules
The unique, unique_except, and exists rules query the database, so they require an async validation pass and a database connection:
use larastvel_core::validation::{validate_async, rules};
use std::collections::HashMap;
use serde_json::json;
let mut data = HashMap::new();
data.insert("email".to_string(), json!("admin@example.com"));
// Resolves the connection from larastvel_core::models::database()
let result = validate_async(&data, vec![
("email", vec![rules::required(), rules::email(), rules::unique("users", Some("email"))]),
]).await;
// Or supply the connection explicitly
let validator = Validator::new(&data, vec![
("email", vec![rules::unique("users", Some("email"))]),
]).with_database(db.clone());
let result = validator.validate_async().await;unique_except ignores a given row id — useful for "update" forms:
rules::unique_except("users", Some("email"), "5") // 5 is the current user's idThe #[validate] attribute macro also validates asynchronously and works with these rules automatically; when no DB-backed rule is present it validates synchronously without needing a connection.
DNS Lookup Faking
The active_url() rule performs a real DNS lookup. Like Laravel 13.22's Validator::fakeDnsLookups(), you can fake lookups for offline tests — only the network call is skipped, malformed URLs still fail:
use larastvel_core::validation::fake_dns_lookups;
// In a test setup:
fake_dns_lookups(true);
// ... run assertions against active_url() ...
// Turn the fake back off (returns the previous state):
fake_dns_lookups(false);email:dns rule
Laravel 13.22's email:dns option is available as the email_dns() rule: the value must be a valid email address and its domain must resolve in DNS (i.e. the mailbox domain actually accepts mail). Like active_url(), the DNS resolution is skipped while fake_dns_lookups(true) is active, so offline tests still exercise the email format check:
use larastvel_core::validation::{validate, rules};
let result = validate(
&json!({ "contact": "not-an-email" }),
&[("contact", &[rules::required(), rules::email_dns()])],
)?;Attribute Macro Validation
Use the #[validate] attribute to validate JSON request bodies directly in handler functions:
use larastvel_core::validate;
use larastvel_core::validation::rules::{required, email, min};
use axum::{Json, extract::Json as JsonExtractor};
use serde_json::{json, Value};
#[validate(vec![
("email", vec![required(), email()]),
("name", vec![required(), min(2)]),
])]
async fn store(Json(body): JsonExtractor<Value>) -> impl IntoResponse {
Json(json!({"ok": true}))
}The macro:
- Finds the
Json<Value>parameter in the handler signature - Converts the body to a
HashMap<String, Value> - Runs the validator; returns
422 Unprocessable Entitywith error details on failure - Passes through to the original handler body on success
Can be combined with #[route]:
#[route]
impl UserController {
#[post("/users")]
#[validate(vec![
("email", vec![required(), email()]),
])]
async fn create(Json(body): JsonExtractor<Value>) -> impl IntoResponse {
Json(json!({"created": true}))
}
}Query String Validation
Use the #[validated_query] attribute to validate query-string parameters:
use larastvel_core::validated_query;
use larastvel_core::validation::rules::{required, min};
use axum::extract::Query;
use std::collections::HashMap;
#[validated_query(vec![
("page", vec![required()]),
("per_page", vec![min(1)]),
])]
async fn list(Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
Json(json!({"page": params.get("page")}))
}Works inside #[route] blocks and composes with #[can] and #[validate]:
#[route]
impl SearchController {
#[get("/search")]
#[can("admin")]
#[validated_query(vec![("q", vec![required()])])]
#[validate(vec![("email", vec![required(), email()])])]
async fn search(
Query(params): Query<HashMap<String, String>>,
Json(body): Json<Value>,
) -> impl IntoResponse {
Json(json!({ "query": params.get("q") }))
}
}Extractor-Based Validation
Use ValidatedJson and ValidatedQuery to auto-validate incoming data:
use larastvel_core::validation::{ValidatedJson, ValidatedQuery};
async fn create_user(ValidatedJson(data): ValidatedJson<CreateUserRequest>) -> Json<User> {
// data is already deserialized
}
async fn search(ValidatedQuery(query): ValidatedQuery<SearchParams>) -> Json<Vec<Result>> {
// query params are validated
}Custom Error Messages
let validator = Validator::new(&data, vec![
("email", vec![rules::required()]),
]).with_messages(HashMap::from([
("email".to_string(), "Please provide your email address.".to_string()),
]));
if validator.fails() {
// handle errors
}Custom Rules
Define custom validation rules with the #[rule] attribute macro. The macro scans the impl block for a validate method and auto-generates the ValidationRule trait implementation; the name() method is derived automatically from the struct name.
use larastvel_core::rule;
use larastvel_core::validation::ValidationError;
#[derive(Debug, Clone)]
struct UpperCaseRule;
#[rule]
impl UpperCaseRule {
fn validate(&self, field: &str, value: &str) -> Result<(), ValidationError> {
if value.chars().any(|c| c.is_lowercase()) {
return Err(ValidationError::new(format!(
"The {} must be uppercase.",
field
)));
}
Ok(())
}
}Use custom rules alongside built-in rules via custom():
use larastvel_core::validation::{validate, rules, custom};
use std::sync::Arc;
let data = /* ... */;
let result = validate(&data, vec![
("code", vec![
rules::required(),
custom(Arc::new(UpperCaseRule)),
]),
]);Generate a new custom rule with make rule:
cargo run make rule UpperCaseValidationErrors API
| Method | Description |
|---|---|
has(field) | Check if field has errors |
first(field) | Get first error for field |
all() | Get all errors |
is_empty() | Check if no errors |
to_json() | Serialize to JSON |