Structs

schema! macro

Declare validated structs with vld::schema!, rename fields, nest structs and add validation to existing types.

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

ItemPurpose
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 Userused 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) — use T as a field schema; errors keep their path (.address.city).
  • vld::nested!(T) — the same, and with openapi registers T as 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 in refine / transform must 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 with vld::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!({})).