Structs

#[derive(Validate)]

The derive macro for structs and enums — field attributes, serde renames and tagging, skip and default, generics and lenient parsing.

With the derive feature, #[derive(Validate)] generates the same API as schema! from attributes on an ordinary struct or enum. Use it when the struct already has other derives (serde, clap, utoipa) or you prefer attributes.

Cargo.toml
vld = { version = "0.5", features = ["derive"] }
use vld::prelude::*;
use vld::Validate;

#[derive(Debug, serde::Serialize, serde::Deserialize, Validate)]
#[serde(rename_all = "camelCase")]
struct SignUp {
    #[vld(vld::string().min(2).max(50))]
    display_name: String,
    #[vld(vld::string().email())]
    email_address: String,
    #[vld(vld::number().int().gte(18).optional())]
    age: Option<i64>,
}

let s = SignUp::vld_parse(r#"{"displayName": "Ada", "emailAddress": "ada@example.com"}"#).unwrap();
assert_eq!(s.age, None);

Every field needs a #[vld(...)] attribute with a schema expression (except #[serde(skip)] fields). The expression is pasted as is, so the paths must be in scope.

Generated API

ItemNotes
T::vld_parse(input)named vld_parse, not parse, so it does not clash with clap::Parser::parse and similar derives
T::parse_value(&value) / T::vld_parse_value(&value)parse a serde_json::Value
impl VldParse for Tused by all integration crates
T::validate(&instance) / T::is_valid(&instance)check a Serialize value
T::json_schema() / T::to_openapi_document()feature openapi
T::validate_fields(..) / T::parse_lenient(..) (+ _value variants)only with #[vld(lenient)]

Serde attributes

AttributeEffect
struct rename_all = "..."lowercase, UPPERCASE, camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE; also rename_all(serialize = "...", deserialize = "...")
field rename = "..."also rename(serialize = "...", deserialize = "...")
field skip / skip_deserializingnot validated, set to Default::default()
field default / default = "path", struct defaulta missing key takes the default without running the schema (as serde does); a present key is validated
field flattenthe field is parsed from the same object (<T>::parse_value, or the field's #[vld(...)] schema); errors keep the inner paths (.author, not .meta.author); a derived / schema! type's properties and required are merged into the JSON Schema (an opaque schema goes into allOf). Not with #[vld(lenient)]; validate() checks the flattened part under its deserialize names
aliascompile error (not supported)

Parsing and JSON Schema use the deserialize names; validate() uses the serialize names. See parse vs validate.

Enums

Enums follow serde's representations, so the same type deserializes and validates the same JSON. The output is the enum itself.

#[derive(Debug, serde::Serialize, serde::Deserialize, Validate)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Action {
    Ping,
    CreateUser {
        #[vld(vld::string().min(2))]
        name: String,
        #[vld(vld::string().email())]
        email: String,
    },
    Move(Point),                       // a derived / schema! type validates itself
    #[serde(rename = "delete", alias = "remove")]
    DeleteUser {
        #[vld(vld::number().int().min(1))]
        id: i64,
    },
}

let a = Action::vld_parse(r#"{"type": "create_user", "name": "Ada", "email": "ada@example.com"}"#)?;
let err = Action::vld_parse(r#"{"type": "fly"}"#).unwrap_err();
assert_eq!(err.issues[0].code.key(), "invalid_discriminator"); // at `.type`
RepresentationInputErrors at
external (default)"Ping", {"Move": {...}}.Move.x
tag = "type"{"type": "move", "x": 1}.type, .x
tag = "t", content = "c"{"t": "move", "c": {...}}.t, .c.x
untaggedthe variant's own shape, tried in orderinvalid_union + each variant's issues ([variant N])

Variants:

  • unit: no content (null / absent);
  • newtype V(T): #[vld(...)] on the field, or none when T has parse_value (#[derive(Validate)] / schema!): vld::nested!(T) is used, and the JSON Schema references T;
  • tuple V(A, B): a JSON array, #[vld(...)] on every field (not with internal tagging, as in serde);
  • struct V { .. }: fields exactly as in a struct (#[vld(...)], rename, default, skip).

Serde attributes: enum tag, content, untagged, rename_all (variant names), rename_all_fields; variant rename, alias, skip / skip_deserializing, other (unit variant for unknown tags), rename_all (its fields). Variant-level untagged, with and deserialize_with are compile errors. Unknown fields are ignored, as in structs.

The JSON Schema is a oneOf with an OpenAPI discriminator for tagged enums, anyOf for untagged ones. #[vld(lenient)] is struct-only.

Generics

Generic structs work. Their field schemas are rebuilt on every call (non-generic structs cache them, see below).

use vld::schema::VldParse;

#[derive(Debug, Validate)]
pub struct Page<T: VldParse> {
    #[vld(vld::array(vld::nested(T::vld_parse_value)))]
    pub items: Vec<T>,
}

Lenient parsing

#[vld(lenient)] on the struct adds validate_fields() and parse_lenient(). Every field type must implement Default and Serialize. See Lenient parsing.

#[derive(Debug, Default, serde::Serialize, Validate)]
#[vld(lenient)]
struct Config {
    #[vld(vld::string().min(1))]
    host: String,
    #[vld(vld::number().int().min(1).max(65535))]
    port: i64,
}

let r = Config::parse_lenient_value(&serde_json::json!({"host": "h", "port": 0})).unwrap();
assert_eq!(r.error_count(), 1);
assert_eq!(r.value.port, 0); // invalid field fell back to Default

OpenAPI parameters

For utoipa::IntoParams, add #[into_params(parameter_in = Query)] (or Path, Header, Cookie) to the struct; the derive records it and vld-utoipa uses it.

Caching

Field schemas of non-generic structs are built once per process and cached, exactly as with schema!: they must be Send + Sync + 'static, and values captured on first use are frozen.