#[derive(Validate)]
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.
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
| Item | Notes |
|---|---|
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 T | used 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
| Attribute | Effect |
|---|---|
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_deserializing | not validated, set to Default::default() |
field default / default = "path", struct default | a missing key takes the default without running the schema (as serde does); a present key is validated |
field flatten | the 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 |
alias | compile 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`
| Representation | Input | Errors 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 |
untagged | the variant's own shape, tried in order | invalid_union + each variant's issues ([variant N]) |
Variants:
- unit: no content (
null/ absent); - newtype
V(T):#[vld(...)]on the field, or none whenThasparse_value(#[derive(Validate)]/schema!):vld::nested!(T)is used, and the JSON Schema referencesT; - 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.