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

Validation is the trust boundary. At the point a handler receives a validated value, it has passed validator::Validate::validate, and the handler does not re-check it.

Validation does not imply authorization. A validated request is a well-formed one, not a permitted one; authorization is a separate explicit step, covered in Authentication.

Declaring a request

use arcature::{Deserialize, Serialize};

#[derive(Debug, Clone, Deserialize, Serialize)]
#[arcature::request]
pub struct StoreLinkRequest {
    #[validate(url)]
    pub url: String,
    #[validate(length(min = 1, max = 120))]
    pub title: String,
}

Two details that are easy to get wrong.

The rule attribute is #[validate(...)], not #[rule(...)]. #[request] prepends #[derive(::arcature::validator::Validate)], and #[validate] is that derive’s helper attribute, so the rule vocabulary is the validator crate’s: required, email, url, length, range, regex, contains, custom, nested.

You derive Deserialize yourself. The macro deliberately does not add it, to avoid a duplicate derive when you also want Serialize or Debug. The #[arcature::request] attribute goes after the derives.

Because the macro re-exports validator through Arcature, an application does not need validator as a direct dependency.

What the macro emits

Three things beside the struct:

  • #[derive(Validate)] and #[validate(crate = "::arcature::validator")].
  • impl arcature::Request, the marker that makes the type first-class to tooling.
  • impl arcature::RequestMetadata, a &'static [FieldShape] describing the fields, which routes! resolves when a route declares action: T so the typed input shape lands in the RouteDescriptor.

Using it in a handler

use arcature::Validated;

pub async fn store(input: Validated<StoreLinkRequest>) -> Result<Response> {
    let data = input.into_inner();
    Ok(redirect().to("/links").into_response())
}

Validated<T> extracts a JSON body, deserializes it, and validates it before the handler body runs. A failure never reaches the handler; it becomes a response.

Four narrower extractors exist for the other sources:

ExtractorSource
ValidatedJson<T>JSON body
ValidatedForm<T>form body
ValidatedQuery<T>query string
ValidatedPath<T>path parameters

Validated<T> delegates to ValidatedJson<T>.

For a value you extracted yourself, validate_or_problem(&value) validates it and returns Err(Problem) on failure.

What a failure looks like

A validation failure is an RFC 9457 problem document, 422, with the field errors under an errors extension:

{
  "type": "urn:arcature:problem:validation",
  "status": 422,
  "detail": "Request validation failed",
  "errors": {
    "url": [{ "code": "url" }],
    "title": [{ "code": "length" }]
  }
}

Extractor rejections — malformed JSON, a missing query parameter, a path segment that will not parse — are mapped to problem documents too, by from_json_rejection and friends, so a client sees one error shape rather than two.

validation_problem(errors) builds the document from a validator::ValidationErrors directly if you need to raise one by hand.