vld
Type-safe runtime validation for Rust, inspired by Zod.
vld combines schema definition with type-safe parsing. Define your validation
rules once and get both runtime checks and strongly-typed Rust structs.
Features
- Zero-cost schema definitions — the
schema!macro generates plain Rust structs with built-inparse()methods. Or useschema_validated!to get lenient parsing too. - Error accumulation — all validation errors are collected, not just the first one.
- Rich primitives — string, number, integer, boolean, literal, enum, any, custom.
- String formats — email, URL, UUID, IPv4, IPv6, Base64, ISO date/time/datetime, hostname, CUID2, ULID, Nano ID, emoji.
All validated without regex by default. Every check has a
_msgvariant for custom messages. - Composable — optional, nullable, nullish, default, catch, refine, super_refine, transform, pipe, preprocess, describe,
.or(),.and(). - Collections — arrays, tuples (up to 6), records, Map (
HashMap), Set (HashSet). - Unions —
union(a, b),union3(a, b, c),.or(),discriminated_union("field"),intersection(a, b),.and(). - Recursive schemas —
lazy()for self-referencing data structures (trees, graphs). - Dynamic objects —
strict(),strip(),passthrough(),pick(),omit(),extend(),merge(),partial(),required(),catchall(),keyof(). - Custom schemas —
vld::custom(|v| ...)for arbitrary validation logic. - Multiple input sources — parse from
&str,String,&[u8],Path,PathBuf, orserde_json::Value. - Validate existing values —
.validate(&value)and.is_valid(&value)work with anySerializetype.schema!structs getStruct::validate(&instance). - Lenient parsing —
parse_lenient()returnsParseResult<T>with the struct, per-field diagnostics, and.save_to_file(). - Error formatting —
prettify_error,flatten_error,treeify_errorutilities. - Custom error messages —
_msgvariants,type_error(), andwith_messages()for per-check and bulk message overrides, including translations. - JSON Schema / OpenAPI —
JsonSchematrait on all schema types;json_schema()andto_openapi_document()onschema!structs;field_schema()for rich object property schemas;to_openapi_document_multi()helper. - Derive macro —
#[derive(Validate)]with#[vld(...)]attributes (optionalderivefeature). - Benchmarks — criterion-based benchmarks included.
- CI — GitHub Actions workflow for testing, clippy, and formatting.
- Minimal dependencies — only
serdeandserde_json. Regex and derive macro are optional features.
Quick Start
Add to your Cargo.toml:
[]
= "0.1"
Optional Features
All features are disabled by default:
| Feature | Description |
|---|---|
serialize |
Adds #[derive(Serialize)] on error/result types, enables VldSchema::validate()/is_valid(), ParseResult::save_to_file()/to_json_string()/to_json_value() |
deserialize |
Adds #[derive(Deserialize)] on error/result types |
openapi |
Enables JsonSchema trait, to_json_schema(), json_schema(), to_openapi_document(), field_schema() |
diff |
Schema diffing — compare two JSON Schemas to detect breaking vs non-breaking changes |
regex |
Custom regex patterns via .regex() (uses regex-lite) |
derive |
#[derive(Validate)] procedural macro |
chrono |
ZDate / ZDateTime types with chrono parsing |
Enable features as needed:
[]
= { = "0.1", = ["serialize", "openapi"] }
Basic Usage
use *;
// Define a validated struct
schema!
// Parse from a JSON string
let user = parse.unwrap;
assert_eq!;
assert_eq!;
// Errors are accumulated
let err = parse.unwrap_err;
assert!;
Nested Structs
use *;
schema!
schema!
let user = parse.unwrap;
Primitives
String
string
.min // minimum length
.max // maximum length
.len // exact length
.email // email format
.url // URL format (http/https)
.uuid // UUID format
.ipv4 // IPv4 address
.ipv6 // IPv6 address
.base64 // Base64 string
.iso_date // ISO 8601 date (YYYY-MM-DD)
.iso_time // ISO 8601 time (HH:MM:SS)
.iso_datetime // ISO 8601 datetime
.hostname // valid hostname
.cuid2 // CUID2 format
.ulid // ULID format (26 chars, Crockford Base32)
.nanoid // Nano ID format (alphanumeric + _-)
.emoji // must contain emoji
.starts_with // must start with
.ends_with // must end with
.contains // must contain
.non_empty // must not be empty
.trim // trim whitespace before validation
.to_lowercase // convert to lowercase
.to_uppercase // convert to uppercase
.coerce // coerce numbers/booleans to string
Number
number
.min // minimum (inclusive)
.max // maximum (inclusive)
.gt // greater than (exclusive)
.lt // less than (exclusive)
.positive // > 0
.negative // < 0
.non_negative // >= 0
.finite // not NaN or infinity
.multiple_of
.safe // JS safe integer range (-(2^53-1) to 2^53-1)
.int // switch to integer mode (i64)
.coerce // coerce strings/booleans to number
Integer
number.int
.min
.max
.gte
.positive
Boolean
boolean
.coerce // "true"/"false"/"1"/"0" -> bool
Literal
literal // exact string match
literal // exact integer match
literal // exact boolean match
Enum
enumeration
Any
any // accepts any JSON value
Modifiers
// Optional: null/missing -> None
string.optional
// Nullable: null -> None
string.nullable
// Nullish: both optional + nullable
string.nullish
// Default: null/missing -> default value
string.with_default
// Catch: ANY error -> fallback value
string.min.catch
Collections
Array
array
.min_len
.max_len
.len // exact length
.non_empty // alias for min_len(1)
Tuple
// Tuples of 1-6 elements
let schema = ;
let = schema.parse.unwrap;
Record
record
.min_keys
.max_keys
Map
// Input: [["a", 1], ["b", 2]] -> HashMap
map
Set
// Input: ["a", "b", "a"] -> HashSet {"a", "b"}
set
.min_size
.max_size
Combinators
Union
// Union of 2 types
let schema = union;
// Returns Either<String, i64>
// Union of 3 types
let schema = union3;
// Returns Either3<String, f64, bool>
union! macro
For convenience, use the union! macro to combine 2–6 schemas without
nesting calls manually. The macro dispatches to union() / union3() or
nests them automatically for higher arities:
use *;
// 2 schemas — same as vld::union(a, b)
let s2 = union!;
// 3 schemas — same as vld::union3(a, b, c)
let s3 = union!;
// 4 schemas — nested automatically
let s4 = union!;
// 5 and 6 schemas work the same way
let s5 = union!;
You can also use the method chaining equivalent .or() for two schemas:
let schema = string.or;
Discriminated Union
// Efficient union by discriminator field
let schema = discriminated_union
.variant_str
.variant_str;
Intersection
// Input must satisfy both schemas
let schema = intersection;
Refine
number.int.refine
Super Refine
// Produce multiple errors in one check
string.super_refine
Transform
string.transform // String -> usize
Pipe
// Chain schemas: output of first -> input of second
string
.transform
.pipe
Preprocess
preprocess
Lazy (Recursive)
// Self-referencing schemas for trees, graphs, etc.
Describe
// Attach metadata (does not affect validation)
string.min.describe
Dynamic Object
For runtime-defined schemas (without compile-time type safety):
let obj = object
.field
.field
.strict; // reject unknown fields
// .strip() // remove unknown fields (default)
// .passthrough() // keep unknown fields as-is
// Object manipulation
let base = object.field.field;
base.pick // keep only "a"
base.omit // remove "b"
base.partial // all fields become optional
base.required // all fields must not be null (opposite of partial)
base.deep_partial // partial (nested objects: apply separately)
base.extend // merge fields from another schema
base.merge // alias for extend
base.catchall // validate unknown fields with a schema
base.keyof // Vec<String> of field names
Per-Field Validation & Lenient Parsing
Use schema_validated! for zero-duplication, or schema! + impl_validate_fields! separately:
use *;
// Option A: single macro (requires Serialize + Default on field types)
schema_validated!
// Option B: separate macros (more control)
// vld::schema! { ... }
// vld::impl_validate_fields!(User { name: String => ..., });
validate_fields — per-field diagnostics
let results = validate_fields.unwrap;
for f in &results
// Output:
// ✖ name: String must be at least 2 characters (received: "X")
// ✖ email: Invalid email address (received: "bad")
// ✔ age: null
parse_lenient — returns a ParseResult<T>
parse_lenient returns a [ParseResult<T>] — a wrapper around the struct and
per-field diagnostics. You can inspect it, convert to JSON, or save to a file
whenever you want.
let result = parse_lenient.unwrap;
// Inspect
println!; // false
println!; // 2
println!; // User { name: "", email: "", age: None }
// Per-field diagnostics
for f in result.fields
// Only errors
for f in result.error_fields
// Display trait prints a summary
println!;
// Convert to JSON string
let json = result.to_json_string.unwrap;
// Save to file at any time
result.save_to_file.unwrap;
// Or extract the struct
let user = result.into_value;
ParseResult<T> methods:
| Method | Description |
|---|---|
.value |
The constructed struct (invalid fields use Default) |
.fields() |
All per-field results (&[FieldResult]) |
.valid_fields() |
Only passed fields |
.error_fields() |
Only failed fields |
.is_valid() |
true if all fields passed |
.has_errors() |
true if any field failed |
.valid_count() |
Number of valid fields |
.error_count() |
Number of invalid fields |
.save_to_file(path) |
Serialize to JSON file (requires Serialize) |
.to_json_string() |
Serialize to JSON string |
.to_json_value() |
Serialize to serde_json::Value |
.into_value() |
Consume and return the inner struct |
.into_parts() |
Consume and return (T, Vec<FieldResult>) |
Single-Field Extraction
Parse the entire schema first, then extract individual fields from the result.
Use parse_lenient + .field("name") to inspect a specific field's validation
status — even when other fields are invalid:
use *;
// Define and register per-field validation
schema!
impl_validate_fields!;
// Strict parse — access fields directly
let user = parse.unwrap;
println!; // "Alex"
// Lenient parse — some fields may be invalid
let result = parse_lenient.unwrap;
// The struct is always available (invalid fields use Default)
println!; // 25 — valid, kept as-is
// Check a specific field
let name_field = result.field.unwrap;
println!; // ✖ name: String must be at least 2 characters
println!; // false
let age_field = result.field.unwrap;
println!; // ✔ age: 25
println!; // true
Error Formatting
use ;
match parse
Input Sources
Schemas accept any type implementing VldInput:
// JSON string
parse?;
// serde_json::Value
let val = json!;
parse?;
// File path
parse?;
// Byte slice
parse?;
Validate Existing Rust Values
Requires the
serializefeature.
Instead of only parsing JSON, you can validate any existing Rust value using
.validate() and .is_valid(). The value is serialized to JSON internally,
then validated against the schema.
On any schema
use *;
// Validate a Vec
let schema = array.min_len.max_len;
assert!;
assert!;
// Validate a String
let email = string.email;
assert!;
assert!;
// Validate a number
let age = number.int.min.max;
assert!;
assert!;
// Validate a HashMap
let schema = record;
let mut map = new;
map.insert;
assert!;
On schema! structs
Structs with #[derive(serde::Serialize)] get validate() and is_valid()
that check an already-constructed instance against the schema:
use *;
schema!
// Construct a struct normally (bypassing parse)
let user = User ;
// Validate it
assert!;
let err = validate.unwrap_err;
// err contains: .name: too short, .email: invalid
// Also works with serde_json::Value or any Serialize type
let json = json!;
assert!;
impl_rules! — Attach Validation to Existing Structs
Use impl_rules! to add .validate() and .is_valid() to a struct you
already have. No need to redefine it — just list the field rules:
use *;
// No #[derive(Serialize)] or #[derive(Debug)] required
impl_rules!;
let p = Product ;
assert!;
let bad = Product ;
assert!;
let err = bad.validate.unwrap_err;
for issue in &err.issues
// .name: String must be at least 2 characters
// .price: Number must be positive
// .quantity: Number must be non-negative
// .tags[0]: String must be at least 1 characters
The struct itself does not need Serialize or Debug — each field is
serialized individually (standard types like String, f64, Vec<T> already
implement Serialize). You can use all schema features inside impl_rules!:
with_messages(), type_error(), refine(), etc.
Chain Syntax: .or() / .and()
// Union via method chaining
let schema = string.or;
// Equivalent to vld::union(vld::string(), vld::number().int())
// Intersection via method chaining
let bounded = string.min.and;
// Input must satisfy both constraints
Custom Schema
Create a schema from any closure:
let even = custom;
assert_eq!;
assert!;
JSON Schema / OpenAPI Generation
Requires the
openapifeature.
Generate JSON Schema (compatible with OpenAPI 3.1)
from any vld schema via the JsonSchema trait:
use *; // imports JsonSchema trait
// Any individual schema
let js = string.min.max.email.json_schema;
// {"type": "string", "minLength": 2, "maxLength": 50, "format": "email"}
// Collections
let js = array.min_len.json_schema;
// {"type": "array", "items": {"type": "integer", ...}, "minItems": 1}
// Modifiers (optional wraps with oneOf)
let js = string.email.optional.json_schema;
// {"oneOf": [{"type": "string", "format": "email"}, {"type": "null"}]}
// Unions → oneOf, Intersections → allOf
let js = union.json_schema;
// {"oneOf": [{"type": "string"}, {"type": "number"}]}
Object field schemas
Use field_schema() (instead of field()) to include full JSON Schema for
each property:
let js = object
.field_schema
.field_schema
.strict
.json_schema;
// {"type": "object", "properties": {"email": {...}, "score": {...}}, ...}
schema! macro — struct-level JSON Schema
Structs defined via schema! automatically get json_schema() and
to_openapi_document() class methods:
use *;
schema!
// Full JSON Schema for the struct
let schema = json_schema;
// {
// "type": "object",
// "required": ["name", "email", "age"],
// "properties": {
// "name": {"type": "string", "minLength": 2, "maxLength": 100},
// "email": {"type": "string", "format": "email"},
// "age": {"type": "integer", "minimum": 0}
// }
// }
// Wrap in a minimal OpenAPI 3.1 document
let doc = to_openapi_document;
// {"openapi": "3.1.0", "components": {"schemas": {"User": {...}}}, ...}
Multi-schema OpenAPI document
use to_openapi_document_multi;
let doc = to_openapi_document_multi;
JsonSchema trait
The trait is implemented for all core types: ZString, ZNumber, ZInt,
ZBoolean, ZEnum, ZAny, ZArray, ZRecord, ZSet, ZObject,
ZOptional, ZNullable, ZNullish, ZDefault, ZCatch, ZRefine,
ZTransform, ZDescribe, ZUnion2, ZUnion3, ZIntersection,
NestedSchema.
Custom Error Messages
Error messages are configured at the schema level, not after validation. There are three mechanisms:
1. _msg variants — per-check custom messages
Every validation method has a _msg variant that accepts a custom error message:
use *;
let schema = string
.min_msg
.max_msg
.email_msg;
let err = schema.parse.unwrap_err;
// -> "Name must be at least 3 characters"
// -> "Please enter a valid email"
Available on all string checks (email_msg, url_msg, uuid_msg, ipv4_msg, etc.)
and number checks are set via with_messages (see below).
2. type_error() — custom type mismatch message
Override the "Expected X, received Y" message when the input has the wrong JSON type:
use *;
let schema = string.type_error;
let err = schema.parse.unwrap_err;
assert!;
let schema = number.type_error;
let schema = number.int.int_error;
3. with_messages() — bulk override by check key
Override multiple messages at once using check category keys. The closure receives
the key and returns Some(new_message) to replace, or None to keep the original:
use *;
let schema = string.min.max.email
.with_messages;
Works on numbers too — great for translations:
use *;
let schema = number.min.max
.with_messages;
For integers, the key "not_int" overrides the "not an integer" message:
use *;
let schema = number.int.min.max
.with_messages;
4. In objects — per-field custom messages
Combine type_error() and with_messages() on individual fields:
use *;
let schema = object
.field
.field;
String check keys
| Key | Check |
|---|---|
too_small |
min |
too_big |
max |
invalid_length |
len |
invalid_email |
email |
invalid_url |
url |
invalid_uuid |
uuid |
invalid_regex |
regex |
invalid_starts_with |
starts_with |
invalid_ends_with |
ends_with |
invalid_contains |
contains |
non_empty |
non_empty |
invalid_ipv4 |
ipv4 |
invalid_ipv6 |
ipv6 |
invalid_base64 |
base64 |
invalid_iso_date |
iso_date |
invalid_iso_datetime |
iso_datetime |
invalid_iso_time |
iso_time |
invalid_hostname |
hostname |
invalid_cuid2 |
cuid2 |
invalid_ulid |
ulid |
invalid_nanoid |
nanoid |
invalid_emoji |
emoji |
Number check keys
| Key | Check |
|---|---|
too_small |
min, gt, gte |
too_big |
max, lt, lte |
not_positive |
positive |
not_negative |
negative |
not_non_negative |
non_negative |
not_non_positive |
non_positive |
not_finite |
finite |
not_multiple_of |
multiple_of |
not_safe |
safe |
not_int |
int (ZInt only) |
Derive Macro
Enable the derive feature for #[derive(Validate)]:
[]
= { = "0.1", = ["derive"] }
use Validate;
// Generates: parse(), parse_value(), validate_fields(), parse_lenient()
let user = parse.unwrap;
Optional Regex Support
Requires the
regexfeature.
By default, vld validates all string formats (email, UUID, etc.) without regex.
If you need custom regex patterns via .regex(), enable the regex feature:
[]
= { = "0.1", = ["regex"] }
let schema = string.regex;
Running the Playground
Benchmarks
Workspace Crates
The vld project is organized as a Cargo workspace with several crates:
| Crate | Path | Description |
|---|---|---|
vld |
. |
Core validation library — schemas, parsers, macros, error handling, i18n |
vld-derive |
crates/vld-derive/ |
Procedural macro #[derive(Validate)] for automatic struct validation |
vld-axum |
crates/vld-axum/ |
Axum integration — extractors VldJson, VldQuery, VldPath, VldForm, VldHeaders, VldCookie |
vld-actix |
crates/vld-actix/ |
Actix-web integration — extractors VldJson, VldQuery, VldPath, VldForm, VldHeaders, VldCookie |
vld-diesel |
crates/vld-diesel/ |
Diesel ORM integration — Validated<S, T> wrapper, VldText, VldInt column types |
vld-utoipa |
crates/vld-utoipa/ |
utoipa integration — auto-generate ToSchema from vld definitions |
vld-config |
crates/vld-config/ |
Config validation — validate TOML/YAML/JSON/ENV via config-rs or figment |
vld-tower |
crates/vld-tower/ |
Universal Tower middleware — validate JSON bodies in any Tower-compatible framework |
vld-rocket |
crates/vld-rocket/ |
Rocket integration — extractors VldJson, VldQuery, VldForm + JSON error catchers |
vld-poem |
crates/vld-poem/ |
Poem integration — extractors VldJson, VldQuery, VldForm |
vld-warp |
crates/vld-warp/ |
Warp integration — filters vld_json, vld_query + handle_rejection |
vld-sea |
crates/vld-sea/ |
SeaORM integration — validate ActiveModel before insert/update |
vld-clap |
crates/vld-clap/ |
Clap integration — validate CLI arguments via #[derive(Validate)] |
vld-ts |
crates/vld-ts/ |
TypeScript codegen — generates Zod schemas from vld JSON Schema output |
vld-derive
Enable with features = ["derive"]. Provides #[derive(Validate)] with #[vld(...)] attributes
on struct fields. Supports #[serde(rename)] and #[serde(rename_all)] for JSON key mapping.
use Validate;
vld-axum
Validation extractors for Axum web framework. Automatically validates request data
and returns 422 Unprocessable Entity with structured JSON errors on failure.
[]
= "0.1"
use *;
async
vld-actix
Validation extractors for Actix-web framework. Same API surface as vld-axum.
[]
= "0.1"
use *;
async
vld-diesel
Integration with Diesel ORM. Validate data before insert/update, validate rows after load,
and use validated column types (VldText<S>, VldInt<S>) that enforce constraints at the type level.
[]
= { = "0.1", = ["sqlite"] }
use *;
let validated = ?;
insert_into.values.execute?;
vld-tower
Universal Tower middleware for JSON body validation. Works with any Tower-compatible framework (Axum, Hyper, Tonic, Warp).
[]
= "0.1"
use ;
use ServiceBuilder;
schema!
// One layer covers all routes
let svc = new
.layer
.service_fn;
// In handler — zero-cost extraction from extensions
let user: CreateUser = validated;
vld-config
Validate configuration files at load time. Supports config-rs (TOML, YAML, JSON, ENV) and figment.
[]
= "0.1"
= "0.1" # config-rs by default
# vld-config = { version = "0.1", features = ["figment"] }
use *;
schema!
let config = builder
.add_source
.add_source
.build.unwrap;
let settings: Settings = from_config.unwrap;
vld-utoipa
Bridge between vld and utoipa. Define validation once, get
ToSchema for free — no need to duplicate schema definitions.
[]
= { = "0.1", = ["openapi"] }
= "0.1"
= "5"
use *;
use impl_to_schema;
schema!
impl_to_schema!;
// Now CreateUser implements utoipa::ToSchema
vld-rocket
Rocket integration with validation extractors and JSON error catchers.
[]
= "0.1"
= { = "0.5", = ["json"] }
use *;
schema!
vld-poem
Poem integration — extractors VldJson, VldQuery, VldForm.
[]
= "0.1"
= "3"
use handler;
use *;
schema!
async
vld-warp
Warp integration — filters vld_json, vld_query + handle_rejection.
[]
= "0.1"
= "0.3"
use *;
use Filter;
schema!
let route = post
.and
.and
.map
.recover;
vld-sea
SeaORM integration — validate ActiveModel before insert/update.
[]
= "0.1"
= "1"
use Set;
use *;
schema!
let am = ActiveModel ;
// Validate before insert
?;
// Or hook into before_save automatically:
// vld_sea::impl_vld_before_save!(ActiveModel, UserInput);
vld-clap
Clap integration — validate CLI arguments via #[derive(Validate)] directly on the struct.
[]
= "0.1"
= { = "0.1", = ["derive"] }
= { = "4", = ["derive"] }
= { = "1", = ["derive"] }
use Parser;
use Validate;
use *;
vld-ts
Generate TypeScript Zod schemas from JSON Schema output produced by vld.
Useful for sharing validation contracts between Rust backend and TypeScript frontend.
[]
= "0.1"
use generate_zod;
let json_schema = json!;
let ts_code = generate_zod;
// Output: export const UserSchema = z.object({ name: z.string().min(2), email: z.string().email() });
License
MIT