Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Validation

Vyuh validation is explicit at the route boundary. Parsing and validation are separate steps:

  • Json<T>, Query<T>, Path<T>, and Form<T> parse request data.
  • Valid<E> opts a parsed extractor into runtime validation.
  • #[derive(Validate)] implements the validation rules.
  • ValidationSchema exposes accurately representable rules to OpenAPI.

This keeps API behavior visible in the handler signature:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;

async fn parse_only(Json(input): Json<CreateUser>) {
    // parsed only
}

async fn parse_and_validate(Valid(Json(input)): Valid<Json<CreateUser>>) {
    // parsed and validated
}
}

Mental Model

  • Request wrappers parse.
  • Valid<E> validates.
  • Validate decides runtime correctness.
  • ValidationSchema describes supported constraints for OpenAPI.
  • Unsupported or business-specific rules never appear in OpenAPI unless they opt in with explicit custom_schema vendor metadata.

Defining Rules

Derive Validate on request DTOs and add #[validate(...)] attributes:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;

#[derive(Deserialize, JsonSchema, Validate)]
struct CreateUser {
    #[validate(email)]
    email: String,

    #[validate(min_length = 3, max_length = 80)]
    name: String,

    #[validate(min = 18)]
    age: u8,
}
}

Deriving Validate alone does not change route behavior. A route validates only when the argument uses Valid<E>.

Supported derive rules:

CategoryRules
Stringsmin_length, max_length, exact_length, pattern
Formatsemail, url, uuid, phone_e164, ipv4, ipv6, date, datetime
Numbersmin, max, exclusive_min, exclusive_max, multiple_of
Collectionsmin_items, max_items, unique_items
Choicesenum_values(...)
Nested datadelegate
Custom logiccustom = "path", optional custom_schema = "name"

min_length, max_length, and exact_length count Unicode characters. They match JSON Schema minLength and maxLength semantics, not UTF-8 byte length.

Using Valid

Valid<E> is generic over request wrappers:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;

async fn create(Valid(Json(input)): Valid<Json<CreateUser>>) {}
async fn search(Valid(Query(input)): Valid<Query<SearchParams>>) {}
async fn update(Valid(Path(input)): Valid<Path<UserPath>>) {}
async fn login(Valid(Form(input)): Valid<Form<LoginForm>>) {}
}

The wrapped type must implement Validate. If it does not, Rust reports a compile-time trait-bound error at the route signature.

Valid<E> supports Deref and into_inner():

#![allow(unused)]
fn main() {
async fn create(valid: Valid<Json<CreateUser>>) {
    let Json(input) = valid.into_inner();
}
}

Standalone Validation

Validation is not tied to HTTP. Any value whose type implements Validate can be checked directly from tests, services, commands, tasks, or ordinary application code:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;

#[derive(Validate)]
struct CreateUser {
    #[validate(email)]
    email: String,

    #[validate(min_length = 3)]
    name: String,
}

let input = CreateUser {
    email: "bad-email".into(),
    name: "Al".into(),
};

let report = input.validate().expect_err("input should be invalid");
assert!(report.has_error("email"));
assert!(report.has_error("name"));
}

Use validate() directly when a test needs to assert the validation rules without building a site or sending an HTTP request. Valid<E> is the route extractor layer; Validate is the underlying runtime rule implementation.

For field-oriented assertions, inspect the ValidationReport:

#![allow(unused)]
fn main() {
let flat = report.to_field_map_flat();
assert_eq!(flat["name"][0], "Must be at least 3 characters");
}

For structured assertions, use the nested helpers:

#![allow(unused)]
fn main() {
let report = input.validate().expect_err("invalid");
let errors = report.to_nested_errors();
assert_eq!(errors["name"][0]["code"], "min_length");
assert_eq!(errors["name"][0]["params"]["min"], "3");
}

Use to_nested_errors() when tests should assert the same code, message, and params shape returned by ErrorReport. Use to_nested_messages() or to_field_map_flat() for simpler message-only assertions.

Errors

Vyuh preserves the distinction between parsing and validation:

  • Parse or deserialization failure returns 400.
  • Validation failure returns 422.

Validation failures are field-oriented and flow through ErrorReport and the site-wide error handler:

{
  "source": "validation",
  "code": "validation_error",
  "detail": "Validation failed.",
  "errors": {
    "email": [
      {
        "code": "email",
        "message": "Enter a valid email address.",
        "params": {}
      }
    ],
    "non_field_errors": [
      {
        "code": "invalid",
        "message": "The submitted data is invalid.",
        "params": {}
      }
    ],
    "name": [
      {
        "code": "min_length",
        "message": "Ensure this field has at least 3 characters.",
        "params": {
          "min": "3"
        }
      }
    ]
  }
}

For the larger error model, including vyuh::Error, command rendering, and task retry behavior, see Errors.

Applications can replace the rendered response with SiteConf::errors(...). The validation report remains the normalized transport object before rendering. Root-level errors are emitted under non_field_errors.

Nested Validation

Use delegate when a field’s type has its own validation rules:

#![allow(unused)]
fn main() {
#[derive(Deserialize, JsonSchema, Validate)]
struct Address {
    #[validate(min_length = 2)]
    city: String,
}

#[derive(Deserialize, JsonSchema, Validate)]
struct Signup {
    #[validate(delegate)]
    address: Address,
}
}

Nested errors preserve field paths so clients can bind failures to form fields or JSON paths.

Runtime-Only Rules

Some validation belongs only at runtime:

  • custom functions,
  • database checks,
  • authorization checks,
  • cross-record or business rules,
  • rules that cannot be represented exactly in JSON Schema.

Runtime validation is authoritative. OpenAPI is documentation only, so Vyuh emits schema constraints only when they can be represented accurately.

Custom validators are runtime-only by default:

#![allow(unused)]
fn main() {
#[validate(custom = "validate_slug")]
slug: String,
}

A custom validator returns Result<(), ValidationError>:

#![allow(unused)]
fn main() {
use vyuh::ValidationError;

fn validate_slug(value: &String) -> Result<(), ValidationError> {
    if value.chars().all(|ch| ch.is_ascii_lowercase() || ch == '-') {
        Ok(())
    } else {
        Err(ValidationError::new("slug", "Enter a valid slug.")
            .with_param("allowed", "lowercase letters, digits, and dashes"))
    }
}
}

Expose a custom validator name to OpenAPI clients only when the frontend should know about it:

#![allow(unused)]
fn main() {
#[validate(custom = "validate_slug", custom_schema = "slug")]
slug: String,
}

This emits Vyuh vendor metadata:

{
  "x-vyuh-validators": ["slug"]
}

custom_schema is not JSON Schema. It is a client hint for application-specific rules, and it is emitted only when the route uses Valid<E>. It requires custom, accepts a string literal, and does not affect runtime validation.

OpenAPI

OpenAPI validation metadata appears only for Valid<E>:

#![allow(unused)]
fn main() {
async fn parse_only(Json(input): Json<CreateUser>) {}
async fn validated(Valid(Json(input)): Valid<Json<CreateUser>>) {}
}

The first route documents the request shape only. The second route documents the request shape plus supported constraints such as string length, numeric minimum and maximum, patterns, formats, collection limits, enum values, and explicit custom hints from custom_schema.

ValidationSchema is the low-level trait generated by #[derive(Validate)]. Most applications do not need to implement it manually; it exists so advanced types can expose schema constraints in the same way as derive-generated types.

Current Limitations

  • Validation runs only where code explicitly calls validate() or uses Valid<E>.
  • OpenAPI contains only constraints that Vyuh can represent accurately, plus explicit custom_schema vendor hints.
  • Custom validators are ordinary Rust functions; Vyuh does not add a runtime validator registry in this pass.