Skip to content

Validation

Larastvel provides a Laravel-inspired validation system with 25 built-in rules.

Basic Usage

rust
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

RuleDescription
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:

rust
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:

rust
rules::unique_except("users", Some("email"), "5") // 5 is the current user's id

The #[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:

rust
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:

rust
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:

rust
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 Entity with error details on failure
  • Passes through to the original handler body on success

Can be combined with #[route]:

rust
#[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:

rust
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]:

rust
#[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:

rust
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

rust
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.

rust
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():

rust
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:

bash
cargo run make rule UpperCase

ValidationErrors API

MethodDescription
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

Released under the MIT License.