Performance
Schemas are built once
schema!, impl_validate_fields!, impl_rules! and #[derive(Validate)] (non-generic structs) build each field schema once per process, on first use, and keep it in a static. Regexes inside them are compiled once, and message templates are not re-rendered per key.
The trade-off:
- field schemas must be
Send + Sync + 'static; - values captured from runtime state are frozen at the first parse.
// Frozen: `limit` is read on the first parse only.
vld::schema! {
pub struct Comment {
pub text: String => vld::string().max(max_comment_len()),
}
}
If a limit changes at runtime (per tenant, from a database), build that schema yourself with vld::object() and keep it where you keep the configuration.
Generic #[derive(Validate)] structs rebuild their schemas on every call, because a static cannot depend on a type parameter.
Build your own schemas once
Free-standing schemas are values: build them once and reuse them.
use std::sync::LazyLock;
use vld::prelude::*;
static EMAIL: LazyLock<ZString> = LazyLock::new(|| vld::string().trim().email());
fn check(input: &str) -> bool {
EMAIL.parse(input).is_ok()
}
Input is borrowed
parse(&serde_json::Value) borrows the value instead of cloning it. If you already have a Value (from a framework or a message queue), pass it directly instead of re-serializing to a string.
Cheap checks first
All checks run so that every issue is reported, so order does not change the result. It does change the cost of a failing parse slightly: put length limits before expensive formats such as regex, url_strict() or phone_e164_strict().
Unions
vld::union! tries variants in order and collects the errors of every failed variant. If the input carries a tag, a discriminated_union looks the variant up directly.
Benchmarks
The core crate has Criterion benchmarks in crates/vld/benches/validation_bench.rs:
cargo bench -p vld