Guides

Migration

Every breaking change in vld 0.6 and 0.5, with before/after code.

0.5 → 0.6

Regular expressions

regex-lite is no longer part of the public API: string().regex(Regex) / regex_msg and the vld::regex_lite re-export are gone. Use pattern (same regex feature, same issue code invalid_string / Regex, same JSON Schema pattern):

// before
vld::string().regex(vld::regex_lite::Regex::new(r"^[A-Z]{3}$").unwrap())
// after
vld::string().pattern(r"^[A-Z]{3}$")
// an invalid pattern you want to handle instead of panicking
vld::string().pattern(vld::rules::Pattern::new(src)?)

For checks that are easier in Rust than in a regex, see custom rules.

optional, nullable, nullish

  • nullable() fields are now required: an absent key fails with missing_field (a present null is still None). Use nullish() where the key may be left out.
  • optional() no longer adds null to its JSON Schema (TypeScript x?: T); parsing still accepts null.
  • nullable() is in required in JSON Schema (TypeScript x: T | null).

See the table.

Unions serialize as plain values

Either / Either3 are serde untagged now: {"Left": "x"} becomes "x". Code that stored or sent the tagged form must use the bare value.

Removed

RemovedUse
features serialize, deserializenothing (serde support is always on): drop them from features
ZFloat::finite()nothing: JSON has no NaN / infinity
ZObject::field_schema()field()
ZObject::deep_partial()partial() on each nested object
vld-actix::VldJsonErrorVldActixError
vld-diesel::validate_update, vld-sqlx::validate_updatevalidate_insert
vld-surrealdb::validate_recordvalidate_content
vld-http-common::ValidationIssueWithCode / format_issues_with_codeValidationIssue / format_issues
vld-http-common::format_json_parse_error / format_utf8_errorVldHttpError::parse (the adapters already answer with it)
vld-tower::try_validated, vld-rama::try_validatedvalidated()
vld-tauri::validate_event / validate_state / validate_plugin_config / validate_channel_messagevalidate
impl_to_lapin!, impl_to_redis!, impl_to_redis_async!LapinChannel::new(ch), RedisConn::new(conn), AsyncRedisConn::new(conn)
vld-utoipa::impl_into_params!, impl_to_schema_query!, impl_to_schema_path!#[into_params(parameter_in = ...)] + impl_to_schema!(T), or impl_to_schema!(T, query)

Custom DynSchema impls

DynSchema has a new required method dyn_parse_missing (implemented for every VldSchema automatically). Only a hand-written DynSchema impl needs it: return what an absent object key should give.

0.4 → 0.5

vld 0.5 makes features additive, gives every built-in check a typed issue code and aligns all web adapters on one set of status codes. Most applications only need the first three steps. The full list of changes is in the changelog.

1. Cargo features

The std feature is gone (everything it gated is always on). serialize and deserialize are deprecated no-ops and will be removed in 0.6. diff now enables openapi.

# before
vld = { version = "0.4", features = ["std", "serialize", "derive"] }
# after
vld = { version = "0.5", features = ["derive"] }

2. Date backends

chrono, jiff and time are additive; each has its own module. Before, enabling several picked one silently (chrono > jiff > time).

  • With one backend nothing changes: vld::date(), vld::datetime() and the prelude ZDate / ZDateTime still work.
  • With several, call the module:
// before (0.4): whichever backend won
let d = vld::date();
// after (0.5)
let d = vld::chrono::date();
let t = vld::jiff::datetime();
let tod = vld::time::time(); // new: time of day
let ty: vld::chrono::ZDate = vld::chrono::date();

Libraries should always use the module form, because an application can enable a second backend through feature unification.

3. Issue codes

Every built-in check has a typed IssueCode variant; Custom is only for your own codes. TooSmall / TooBig carry origin: SizeOrigin and a JSON bound. IssueCode, StringValidation and DateKind are #[non_exhaustive].

// before
match &issue.code {
    IssueCode::TooSmall { minimum, .. } => println!("min {minimum}"), // f64
    IssueCode::Custom { code } if code == "invalid_length" => {}
    _ => {}
}

// after
match &issue.code {
    IssueCode::TooSmall { origin: SizeOrigin::String, minimum, .. } => {
        println!("min {}", minimum.as_f64().unwrap_or_default()); // serde_json::Value
    }
    IssueCode::InvalidLength { expected, .. } => println!("exactly {expected}"),
    _ => {} // required: the enum is non-exhaustive
}

// simplest and stable:
if issue.code.key() == "too_small" { /* … */ }

New typed variants include InvalidLength, InvalidFormat, InvalidDate, NotMultipleOf, InvalidEnumValue, InvalidUnion, NotUnique, MissingField, FileTypeNotAllowed, SerializeError. See Issue codes.

4. Messages

  • Default messages come from one English template table.
  • An absent required key reports missing_field ("Required field is missing"); a present null is still invalid_type.
  • Display no longer appends ", received X" to invalid_type (the message already says it).

Tests that compare full English messages may need updating. Custom translation tables can add the new keys (missing_field, type.<name>, too_small.<origin>, …); the specific key is tried before the generic one. See Translations.

5. JSON Schema

Optional, nullable and nullish fields and unions emit anyOf instead of oneOf, and nullable() fields are no longer required. Update consumers or snapshots that expect oneOf.

// before
{"oneOf": [{"type": "string"}, {"type": "null"}]}
// after
{"anyOf": [{"type": "string"}, {"type": "null"}]}

The inherent to_json_schema() methods on individual schema types are gone; use the JsonSchema trait (json_schema()). ZObject::to_json_schema() stays.

6. Module paths

The core was split into schema/, error/, object/, macros/ and openapi/. The old paths vld::json_schema, vld::diff, vld::format, vld::i18n and vld::input remain as aliases, so imports keep compiling.

7. Schema caching

schema!, impl_validate_fields!, impl_rules! and #[derive(Validate)] build field schemas once per process. Field schemas must be Send + Sync + 'static, and values captured from runtime configuration are frozen at first use. See Performance.

VldInput::to_json_value now returns Cow<Value> (only relevant if you implemented VldInput).

8. derive

validate_fields() / parse_lenient() are generated only with #[vld(lenient)]. #[serde(flatten)] and #[serde(alias)] are compile errors instead of being ignored.

// after
#[derive(Debug, Default, serde::Serialize, vld::Validate)]
#[vld(lenient)]
struct Config { /* … */ }

9. Web adapters

  • VldJson answers 415 unless the content type is application/json or application/*+json. Clients must send Content-Type: application/json.
  • Malformed JSON is 422 with the validation body (was 400).
  • Each issue in the 422 body has a code field.
  • Query, path, form, header and cookie values fall back to their original text when the schema wants a string (?q=123 into vld::string() is "123").

See the web overview.

10. Crate-specific changes

CrateChangeWhat to do
vld-tower, vld-ramavalidated() returns Option<T>; try_validated is deprecated; the tower inner service receives Request<ValidatedBody<B>> and non-JSON bodies are no longer bufferedhandle None
vld-warphandle_rejection maps rejections to their real status (unknown → 500, was 404); bodies are streamed with a limit (413)check status-code tests
vld-ntexntex 4 is the default; crate MSRV 1.97ntex 3: vld-ntex = { version = "0.5", default-features = false, features = ["ntex-3"] }
vld-tswith_default emits x.nullish().transform((v) => v ?? d) (Zod) / v.nullish(x, d) (Valibot)regenerate client schemas; defaults accept null
vld-clapvalidate_or_exit requires clap::CommandFactory and reports clap's argument names; clap 4derive clap::Parser, or call validate_cli
vld-tauri, vld-leptos, vld-dioxus, vld-surrealdbissue paths have no leading dot (user.email, items[0])update path comparisons
// vld-tower / vld-rama, before: returned T and panicked when missing
let body: CreateUser = vld_tower::validated(&req);
// after: None when the layer did not run or the request was not JSON
let Some(body) = vld_tower::validated::<CreateUser>(&req) else {
    return bad_request();
};

11. MSRV

The core and most crates need Rust 1.85. Some integration features need more; see MSRV.