Web frameworks

Web adapters overview

What every vld web integration has in common — extractors, status codes, the error body, coercion, header and cookie rules, and body limits.

All HTTP adapters are thin wrappers around vld-http-common: they read the request, turn it into a serde_json::Value, validate it with your schema and convert failures into the same JSON error body. Behaviour is checked by one shared conformance suite, so switching frameworks does not change what clients see.

Any type that implements VldParse works as the extractor's T: a vld::schema! struct, a #[derive(Validate)] struct, or one built with impl_rules!. No serde::Deserialize is needed.

Extractors

CrateJSON bodyQueryPathFormHeadersCookiesExtra
vld-axumVldJsonVldQueryVldPathVldFormVldHeadersVldCookie—
vld-actixVldJsonVldQueryVldPathVldFormVldHeadersVldCookieVldBodyLimit
vld-ntexVldJsonVldQueryVldPathVldFormVldHeadersVldCookieVldBodyLimit
vld-poemVldJsonVldQueryVldPathVldFormVldHeadersVldCookieVldBodyLimit
vld-ramaVldJsonVldQueryVldPathVldFormVldHeadersVldCookieValidateJsonLayer, VldBodyLimit
vld-rocketVldJsonVldQueryVldPathVldFormVldHeadersVldCookievld_catchers()
vld-salvoVldJsonVldQueryVldPathVldFormVldHeadersVldCookie—
vld-warpvld_jsonvld_queryvld_param, vld_pathvld_formvld_headersvld_cookiehandle_rejection
vld-towerValidateJsonLayer—————validated()
vld-tonicmessages: validate, validate_with———metadata: validate_metadata—metadata_interceptor

vld-tower and ValidateJsonLayer in vld-rama are middleware: they validate the JSON body before your handler runs and store the result in the request extensions. vld-tonic works with gRPC messages and metadata instead of HTTP requests.

Status codes

StatusWhen
200 (your handler)The value passed the schema.
413 Payload Too LargeA JSON or form body is larger than the body limit.
415 Unsupported Media TypeA JSON extractor got a Content-Type other than application/json or application/*+json, or a form extractor got something other than application/x-www-form-urlencoded (a missing header counts as wrong).
422 Unprocessable EntityValidation failed, or the input could not be parsed at all: malformed JSON, a form body that is not UTF-8, more than 1000 form fields.

Media-type checks ignore case and parameters (Application/JSON; charset=utf-8 is JSON). Malformed JSON is 422, not 400: the body has the same shape as a validation error, with one parse_error issue at the root.

Framework-specific additions:

  • vld-tower and the vld-rama layer pass non-JSON requests through untouched instead of answering 415; validated() returns Option<T>.
  • vld-warp's handle_rejection also maps warp's own rejections (404, 405, 400, 411, 415) and answers 500 for anything unknown.
  • vld-rocket's vld_catchers() adds JSON catchers for 400, 413, 415 and 422 (without them Rocket answers with its HTML error pages).
  • vld-tonic returns tonic::Status with code INVALID_ARGUMENT.

Error body

Every 422 response has this shape (Content-Type: application/json):

{
  "error": "Validation failed",
  "issues": [
    { "path": ".name", "message": "String must be at least 2 characters", "code": "too_small" },
    { "path": ".items[0].qty", "message": "Number must be at least 1", "code": "too_small" }
  ]
}
  • path is "" for the root (for example malformed JSON), .field for object fields and [i] for array items.
  • message is the issue message (default English, your custom message, or a translation if you translate the error before responding).
  • code is the stable IssueCode::key() — match on it in clients instead of on messages.

The other bodies:

StatusBody
413{"error": "Payload too large"}
415{"error": "Unsupported Media Type", "message": "Expected Content-Type: application/json (or application/*+json)"} (forms: "Expected Content-Type: application/x-www-form-urlencoded")

Each adapter's error type (VldJsonRejection, VldActixError, VldNtexError, VldPoemError, VldSalvoError, VldRejection) exposes status(), body() and error() (the underlying VldError) if you want to build your own response.

Text values and coercion

Query strings, path segments, form fields, headers and cookies are text. The adapters convert each value with a lossless rule:

Raw valueJSON value
true, false (lowercase)boolean
canonical integers that fit i64 / u64 (0, 42, -7)number
canonical decimals that round-trip through f64 (3.14, -0.5)number
everything else: "", null, 01234, +1555, 1e3, TRUE, 1.50, ids beyond u64string

Then the value is validated with vld::schema::parse_coerced, which repairs two common mismatches and retries:

  • a field whose schema expects a string but got a coerced number or boolean gets its original text back — ?q=123 into vld::string() is "123", ?zip=01234 was never converted in the first place;
  • a field whose schema expects an array but got a single value is wrapped — ?tags=a into vld::array(vld::string()) is ["a"].

So a schema describes the real types (vld::number().int() for ?page=2, vld::boolean() for ?active=true) and string fields keep working when the text happens to look numeric. JSON bodies are not coerced: {"page": "2"} fails vld::number() unless the schema calls .coerce().

Query strings and forms

  • Keys and values are form-decoded (+ is a space, %XX is decoded as UTF-8; invalid sequences become U+FFFD).
  • A repeated key becomes an array: ?tag=a&tag=b → {"tag": ["a", "b"]}.
  • A key ending in [] is always an array: ?tag[]=a → {"tag": ["a"]}.
  • A key without = (?flag) and an empty value (?x=) are "".
  • Form bodies are parsed the same way and may have at most 1000 fields (more is a 422).

Path parameters

Path parameters are percent-decoded exactly once and coerced like query values. A route without parameters produces an empty object, so a schema whose fields are all optional still validates.

Headers

  • Names are lowercased and - becomes _: X-Request-Id is the field x_request_id.
  • Header names that already contain _ are ignored. Otherwise a client could send x_user: admin and fill the x_user field even behind a proxy that strips only X-User.
  • Repeated headers are joined with ", " before coercion. Non-UTF-8 bytes become U+FFFD.

Cookies

  • Every Cookie header is read (HTTP/2 clients may send one header per cookie); they are joined before parsing.
  • The first occurrence of a name wins (sid=a; sid=b → a). Browsers send the most specific cookie first; letting a later one win would allow cookie tossing from a sibling subdomain.
  • Names are used as-is (a cookie session-id needs a field named session-id, map it with as "session-id" in schema!). Values are unquoted, percent-decoded (+ is kept) and coerced.

Body limits

VldJson and VldForm never buffer more than the limit. The default everywhere is 2 MiB (vld_http_common::DEFAULT_BODY_LIMIT, the same as axum's DefaultBodyLimit), regardless of the framework's own JSON default.

CrateDefaultHow to change
vld-axumaxum DefaultBodyLimit (2 MiB).layer(axum::extract::DefaultBodyLimit::max(n))
vld-actix2 MiBApp::new().app_data(vld_actix::VldBodyLimit(n)) — actix JsonConfig / PayloadConfig do not apply
vld-ntex2 MiBntex 4: App::new().with_config(WebAppConfig::new().set_state(VldBodyLimit(n))); ntex 3: App::new().state(VldBodyLimit(n))
vld-poem2 MiB.data(vld_poem::VldBodyLimit(n)) on a route or endpoint
vld-rama2 MiBextractors: the vld_rama::VldBodyLimit(n) request extension (e.g. AddInputExtensionLayer); layer: ValidateJsonLayer::with_limit(n)
vld-rocket2 MiBRocket limits.json / limits.form (Rocket.toml, ROCKET_LIMITS); a limit left at Rocket's built-in value (1 MiB / 32 KiB) counts as unset
vld-salvo2 MiBSalvo's secure_max_size (SecureMaxSize middleware, Request::set_secure_max_size, set_global_secure_max_size); Salvo's built-in 64 KiB counts as unset
vld-tower2 MiBValidateJsonLayer::with_limit(n)
vld-warp2 MiBvld_json_limit::<T>(n) / vld_form_limit::<T>(n)
vld-tonic—tonic's own max_decoding_message_size

Compressed request bodies: vld-actix decodes Content-Encoding itself (the limit applies to the decoded size), vld-ntex does with its compress feature, the other adapters see the body as the framework delivers it.

OpenAPI

The adapters only validate. To document the same schemas, use vld-utoipa (framework-agnostic), vld-aide (aide, usually with axum) or vld-schemars. The error body structs (ValidationErrorBody, ErrorBody, ErrorWithMessage) live in vld-http-common and are vld::schema! types, so they can be registered as response schemas too.