schema! macro
vld::schema! declares a normal Rust struct together with a schema per field. It needs no feature and no proc-macro.
use vld::prelude::*;
vld::schema! {
#[derive(Debug, Clone, serde::Serialize)]
pub struct User {
pub name: String => vld::string().min(2).max(50),
pub email: String => vld::string().email(),
pub age: Option<i64> => vld::number().int().min(0).optional(),
pub tags: Vec<String> => vld::array(vld::string()).max_len(5).with_default(vec![]),
}
}
let user = User::parse(r#"{"name": "Alice", "email": "alice@example.com", "age": 30}"#).unwrap();
assert_eq!(user.age, Some(30));
assert!(user.tags.is_empty());
Each field is name: Type [as "json_key"] => schema. The schema's output type must be the field type.
Generated API
| Item | Purpose |
|---|---|
User::parse(input) | parse a JSON string, bytes, file path or &serde_json::Value |
User::parse_value(&value) | parse a serde_json::Value |
impl VldParse for User | used by every integration crate (extractors, DB wrappers, …) |
User::validate(&instance) / User::is_valid(&instance) | check an existing value that implements Serialize (see parse vs validate) |
User::json_schema() / User::to_openapi_document() | with feature openapi |
Unknown keys in the input are ignored.
Renaming fields
as "jsonKey" sets the JSON name for both parsing and validation:
vld::schema! {
pub struct ApiUser {
pub first_name: String as "firstName" => vld::string().min(1),
pub last_name: String as "lastName" => vld::string().min(1),
}
}
let u = ApiUser::parse(r#"{"firstName": "John", "lastName": "Doe"}"#).unwrap();
assert_eq!(u.first_name, "John");
Without as, the macro follows serde attributes written inside it: field #[serde(rename = "...")] or rename(serialize = "...", deserialize = "..."), then struct #[serde(rename_all = "...")]. alias, flatten, skip and default are not interpreted by schema! (the derive supports skip and default).
as "key" is invisible to serde. If the struct also derives Serialize, add a matching #[serde(rename)]. Raw identifiers map to their bare name (r#type → "type").
Nested structs
vld::schema! {
#[derive(Debug, Clone)]
pub struct Address {
pub city: String => vld::string().min(1),
}
}
vld::schema! {
#[derive(Debug)]
pub struct Customer {
pub name: String => vld::string(),
pub address: Address => vld::nested(Address::parse_value),
pub billing: Option<Address> => vld::nested!(Address).optional(),
}
}
vld::nested(T::parse_value)— useTas a field schema; errors keep their path (.address.city).vld::nested!(T)— the same, and withopenapiregistersTas an OpenAPI component referenced by$ref. Generic types get a sanitized component name (Page<User>→Page_User_).
Schema caching
Field schemas are built once per process, on first use, and stored in a static. Two consequences:
- Field schemas must be
Send + Sync + 'static(closures inrefine/transformmust not borrow local data). - A schema built from runtime values, such as
vld::string().max(config.limit), freezes the value seen on the first parse. Build such schemas at runtime withvld::object()instead.
Companion macros
schema_validated!
vld::schema_validated! is schema! plus lenient parsing (validate_fields(), parse_lenient()) in one declaration. Field types must implement Serialize and Default.
impl_validate_fields!
Adds validate_fields() / parse_lenient() to a struct already declared with schema!. It repeats the field list:
vld::impl_validate_fields!(User {
name: String => vld::string().min(2).max(50),
email: String => vld::string().email(),
age: Option<i64> => vld::number().int().min(0).optional(),
tags: Vec<String> => vld::array(vld::string()).max_len(5).with_default(vec![]),
});
impl_rules!
Adds validate() and is_valid() instance methods to a struct you did not declare with schema!. Only the listed field types need Serialize; the struct itself needs nothing.
struct Product {
name: String,
price: f64,
}
vld::impl_rules!(Product {
name => vld::string().min(2).max(100),
price => vld::number().positive(),
});
let p = Product { name: "X".into(), price: -1.0 };
assert!(!p.is_valid());
assert_eq!(p.validate().unwrap_err().issues.len(), 2);
Use field as "wireName" => schema to report errors under a JSON name.
impl_default!
vld::impl_default!(Config { host, port }) implements Default using each field type's default (like #[derive(Default)]). Schema defaults from .with_default() are not used; to get them, parse an empty object: Config::parse_value(&serde_json::json!({})).