Introduction
vld is a runtime validation library for Rust, modelled on Zod. You describe the shape of your data with schema values, then parse untrusted input with them. A successful parse returns typed Rust values; a failed one returns a VldError listing every problem with its path.
use vld::prelude::*;
let schema = vld::string().trim().min(2).max(50);
assert_eq!(schema.parse(r#"" Alex ""#).unwrap(), "Alex");
let err = schema.parse(r#""A""#).unwrap_err();
assert_eq!(err.issues[0].code.key(), "too_small");
Why not just serde?
serde turns JSON into Rust types and stops at the first error. It knows nothing about lengths, formats, ranges or cross-field rules, and its errors are strings meant for developers.
vld sits on top of serde_json::Value:
- Constraints live next to the type:
vld::string().email(),vld::u16().min(1),vld::array(...).max_len(10). - All issues at once, each with a path (
items[2].email), a typedIssueCodeand a message you can translate. - Typed output:
schema!structs and#[derive(Validate)]give you plain Rust structs. - One source of truth for docs: every schema can emit JSON Schema / OpenAPI 3.1.
- Integrations for web frameworks, databases, queues, config, CLI and UI crates reuse the same schemas and error format.
Coming from Zod
Most Zod concepts map one to one.
| Zod | vld |
|---|---|
z.string().min(2).email() | vld::string().min(2).email() |
z.number().int().positive() | vld::number().int().positive() or vld::u32().positive() |
z.object({...}) | vld::schema! { struct ... }, #[derive(Validate)] or vld::object() |
z.array(z.string()).max(5) | vld::array(vld::string()).max_len(5) |
z.enum(["a", "b"]) | vld::enumeration(&["a", "b"]) |
z.union([...]) | vld::union!(a, b, c) / a.or(b) |
z.discriminatedUnion("type", ...) | vld::discriminated_union("type").variant(...) |
.optional() / .nullable() / .nullish() | same names |
.default(x) | .with_default(x) |
.catch(x) | .catch(x) |
.refine() / .superRefine() | .refine() / .super_refine() |
.transform() / .pipe() / z.preprocess() | .transform() / .pipe() / vld::preprocess() |
z.lazy() | vld::lazy() |
z.coerce.number() | vld::number().coerce() |
error.flatten() / z.treeifyError() / z.prettifyError() | flatten_error() / treeify_error() / prettify_error() |
zod-to-json-schema | built in: JsonSchema::json_schema() (feature openapi) |
Differences worth knowing:
- Sizes are named after what they count: strings use
min/max(characters), arraysmin_len/max_len, sets and filesmin_size/max_size, objects and recordsmin_keys/max_keys. - Integers are their own schemas (
vld::i32(),vld::u64()…) and are read exactly, without anf64round trip. - Unknown object keys are stripped by default, as in Zod; use
.strict()onvld::object()to reject them. - Unions return
Either<A, B>/Either3<A, B, C>because Rust has no anonymous union types.
Crates
The core crate vld has no required dependencies besides serde and serde_json; heavier validators sit behind features. Around it are integration crates:
- Web:
vld-axum,vld-actix,vld-ntex,vld-poem,vld-rama,vld-rocket,vld-salvo,vld-tower,vld-warp,vld-tonic. See the web overview. - OpenAPI and JSON Schema:
vld-utoipa,vld-schemars,vld-aide. - Codegen and test data:
vld-ts(Zod / Valibot from vld schemas),vld-fake. - Databases:
vld-sqlx,vld-diesel,vld-sea,vld-surrealdb. - Messaging:
vld-redis,vld-lapin. - Config and CLI:
vld-config,vld-clap. - UI:
vld-dioxus,vld-leptos,vld-tauri.
Next: install the crate and follow the quick start.