# Validation

> Validate request bodies and query strings with `Valid<Json<T>>` on garde, with field-level messages, `unique` / `exists` database rules, and international input: phone numbers, names and digits in any script, countries, locales and sanitized rich text.

Navree validates input with extractors. You describe the rules on a DTO with [garde](https://docs.rs/garde), take `Valid<Json<T>>` or `Valid<Query<T>>` in your handler, and the handler only runs when the input passes. When it doesn't, the client gets a 400 [error](https://navree.com/docs/basics/errors.md) with one message per broken rule, such as `email must be a valid email address`.

Validation lives in the `navree-validation` crate (the `validation` feature of the `navree` facade, on by default). `Valid` and `ValidAsync` are in the prelude. The [international types](#international-input) `Phone` and `SafeHtml` come with the `validation-phone` and `validation-html` features, also on by default.

## A first example

### Defining the rules

A DTO is a struct that derives `serde::Deserialize` and `garde::Validate`. Every field gets a `#[garde(...)]` attribute with its rules, or `#[garde(skip)]` when it has none:

```rust title="src/contacts/dto.rs"
use garde::Validate;
use navree::validation::rules;
use serde::Deserialize;

#[derive(Debug, Deserialize, Validate)]
pub struct CreateContact {
    #[garde(length(chars, min = 1, max = 100))]
    pub first_name: String,
    #[garde(length(chars, min = 1, max = 100))]
    pub last_name: String,
    #[garde(email)]
    pub email: String,
    #[garde(range(min = 16, max = 120))]
    pub age: Option<u8>,
    #[garde(length(max = 10), inner(length(min = 1, max = 30)))]
    pub tags: Vec<String>,
    #[garde(custom(rules::one_of(&["lead", "customer", "churned"])))]
    pub status: String,
}

#[derive(Debug, Deserialize, Validate)]
pub struct ContactFilters {
    #[garde(length(min = 2))]
    pub search: Option<String>,
    #[garde(range(min = 1, max = 100))]
    pub per_page: Option<u32>,
}
```

garde's derive refers to `::garde`, so your app depends on `garde` directly. Apps created with `navree new` already do:

```toml title="Cargo.toml"
[dependencies]
garde = { version = "0.23", features = ["derive", "email"] }
```

Add garde's `url`, `regex` or other features when you use the rules that need them.

### Using the extractor

Wrap the usual Axum extractor in `Valid`. The pattern `Valid(Json(input))` hands you the validated value:

```rust title="src/contacts/controller.rs"
use navree::prelude::*;
use serde::Serialize;
use uuid::Uuid;

#[derive(Debug, Serialize)]
pub struct ContactView {
    pub name: String,
}

#[injectable]
pub struct ContactsController;

#[controller("/contacts", version = "1")]
#[authenticated]
impl ContactsController {
    #[get("/")]
    async fn index(&self, Valid(Query(filters)): Valid<Query<ContactFilters>>) -> Json<Vec<String>> {
        Json(filters.search.into_iter().collect())
    }

    #[post("/")]
    async fn store(&self, Valid(Json(input)): Valid<Json<CreateContact>>) -> Created<ContactView> {
        let id = Uuid::now_v7();
        let name = format!("{} {}", input.first_name, input.last_name);
        Created(format!("/api/v1/contacts/{id}"), ContactView { name })
    }
}
```

### What the client receives

Send a body that breaks several rules:

```json title="POST /api/v1/contacts"
{
  "first_name": "",
  "last_name": "Lovelace",
  "email": "ada@",
  "age": 12,
  "tags": ["vip", ""],
  "status": "prospect"
}
```

The handler doesn't run. The client gets this response (formatted here):

```json title="400 Bad Request"
{
  "statusCode": 400,
  "message": [
    "age must not be less than 16",
    "email must be a valid email address",
    "first_name must be longer than or equal to 1 characters",
    "status must be one of the following values: lead, customer, churned",
    "tags.1 must be longer than or equal to 1 characters"
  ],
  "error": "Bad Request",
  "code": "validation_failed",
  "errors": {
    "age": ["age must not be less than 16"],
    "email": ["email must be a valid email address"],
    "first_name": ["first_name must be longer than or equal to 1 characters"],
    "status": ["status must be one of the following values: lead, customer, churned"],
    "tags.1": ["tags.1 must be longer than or equal to 1 characters"]
  }
}
```

The [error response format](#error-responses) section below explains each member.

## The extractors

| Extractor | Reads | Root name for whole-input errors |
|---|---|---|
| `Valid<Json<T>>` | a JSON body (`application/json` or any `application/*+json`) | `body` |
| `Valid<Query<T>>` | the query string | `query` |
| `Valid<Form<T>>` | an `application/x-www-form-urlencoded` body, or the query string for `GET` and `HEAD` | `body` (`query` for `GET` and `HEAD`) |
| `ValidAsync<Json<T>>`, `ValidAsync<Query<T>>`, `ValidAsync<Form<T>>` | the same, then the [database rules](#database-rules) | as above |

`T` must implement `serde::de::DeserializeOwned` and `garde::Validate<Context = ()>`. Each extractor works in two steps:

1. **Deserialize.** serde turns the input into `T`. A missing field, a wrong type or an unknown enum value fails here, as a field error.
2. **Validate.** garde runs every rule of `T` and reports every failure at once.

Some failures are not validation errors and keep their own status: a JSON extractor answers 415 when the `Content-Type` header is not JSON, and a body larger than `http.body_limit_bytes` is a 413. See [errors](https://navree.com/docs/basics/errors.md) for those.

`Form` is Axum's: import it with `use navree::axum::extract::Form;`.

## Available rules

### garde's rules

Every garde rule works. These are the ones you'll use most, with the message Navree renders for a field called `name`:

| Rule | Checks | Message |
|---|---|---|
| `length(min = 2, max = 50)` | string length (bytes) | `name must be longer than or equal to 2 characters`, `name must be shorter than or equal to 50 characters` |
| `length(chars, min = 2)` | string length in characters | as above |
| `length(min = 1, max = 10)` on a list | number of items | `name must contain at least 1 elements`, `name must contain no more than 10 elements` |
| `range(min = 1, max = 100)` | numbers | `name must not be less than 1`, `name must not be greater than 100` |
| `required` | an `Option` is `Some` | `name should not be empty` |
| `email` | an email address | `name must be a valid email address` |
| `url` | a URL (garde feature `url`) | `name must be a URL address` |
| `pattern(r"^[A-Z]{3}$")` | a regular expression (garde feature `regex`) | `name must match ^[A-Z]{3}$ regular expression` |
| `ascii`, `alphanumeric` | character classes | `name must contain only ASCII characters`, `name must contain only letters and numbers` |
| `contains("x")`, `prefix("x")`, `suffix("x")` | substrings | `name must contain a x string`, `name must start with x`, `name must end with x` |
| `ip`, `ipv4`, `ipv6` | IP addresses | `name must be an ip address`, `name must be an IPv4 address`, `name must be an IPv6 address` |
| `matches(other_field)` | equals another field | `name must match other_field` |
| `dive`, `inner(...)` | nested structs, list items | see [nested data](#validating-nested-data) |
| `custom(f)` | your own function | see [custom rules](#custom-rules) |
| `skip` | nothing | |

> **Tip: Count characters, not bytes**
>
> garde's plain `length` counts the bytes of a string, so `"Zoë"` has length 4. Write `length(chars, max = 255)` when you mean characters, as the CRM does.

Rules on an `Option<T>` only run when the value is `Some`; `required` is the rule that demands one. `#[garde(skip)]` is required on fields without rules, unless you put `#[garde(allow_unvalidated)]` on the struct.

### Navree's extra rules

`navree::validation::rules` adds rules garde lacks. Use them with `custom(...)`; they accept `String`, `&str`, `Cow<str>` and `Option`s of them, and `None` passes:

```rust
use navree::validation::confirmed;
use navree::validation::rules;

#[derive(Debug, Deserialize, Validate)]
pub struct Register {
    #[garde(length(min = 8), custom(confirmed(&self.password_confirmation)))]
    pub password: String,
    #[garde(skip)]
    pub password_confirmation: String,
    #[garde(custom(rules::uuid))]
    pub team_id: String,
    #[garde(custom(rules::date_ymd))]
    pub starts_on: Option<String>,
}
```

| Rule | Checks | Message |
|---|---|---|
| `confirmed(&self.x_confirmation)` | the field equals its confirmation; the error goes on the field itself | `password confirmation does not match` |
| `rules::uuid` | a hyphenated UUID | `team_id must be a UUID` |
| `rules::date_ymd` | a real date written exactly `YYYY-MM-DD` | `starts_on must be a valid date in the format YYYY-MM-DD` |
| `rules::one_of(&["a", "b"])` | one of the listed strings | `status must be one of the following values: a, b` |
| `rules::unicode_name` | a name in any script | `name must contain only letters, numbers, spaces, hyphens, apostrophes and periods` |
| `rules::phone` | a valid phone number (feature `validation-phone`) | `phone must be a valid phone number` |
| `rules::country` | an ISO 3166-1 alpha-2 country code | `country must be a valid ISO 3166-1 alpha-2 country code` |
| `rules::locale` | a BCP 47 language tag | `locale must be a valid locale` |

The last four are explained under [international input](#international-input).

### Rules serde checks for you

Typed fields are checked while deserializing, before any garde rule runs. A `Uuid` field rejects `"nope"`, an enum rejects unknown variants, and so on:

| Input problem | Message |
|---|---|
| a required field is missing, or `null` | `title should not be empty` |
| a string where a number is expected | `count must be an integer number`, `ratio must be a number` |
| a number where a string is expected | `title must be a string` |
| not `true` or `false` | `active must be a boolean value` |
| not a list, not an object | `labels must be an array`, `address must be an object` |
| not a UUID | `owner_id must be a UUID` |
| an unknown enum variant | `stage must be one of the following values: lead, won` |
| an unknown field, with `#[serde(deny_unknown_fields)]` | `property extra should not exist` |

So an enum is often all the validation a field needs:

```rust
#[derive(Debug, Deserialize, Validate)]
pub struct NewSubscription {
    #[garde(skip)]
    pub plan: Plan, // "starter", "team" or "enterprise"
    #[garde(range(min = 1))]
    pub seats: u32,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Plan {
    Starter,
    Team,
    Enterprise,
}
```

### Required fields

serde stops at the first problem, so deserialization errors come **one at a time and before the garde rules**. A `CreateContact` body with only `first_name` and `email` is missing three fields, but gets only:

```json title="400 Bad Request"
{
  "statusCode": 400,
  "message": ["last_name should not be empty"],
  "error": "Bad Request",
  "code": "validation_failed",
  "errors": { "last_name": ["last_name should not be empty"] }
}
```

When you want every missing field reported at once, make the fields `Option`s and use garde's `required` rule. The CRM's deal DTO does this (`not_blank` is one of its own [custom rules](#custom-rules), which rejects a whitespace-only title):

```rust title="crm-deals/src/dto.rs"
#[derive(Debug, Default, Deserialize, Validate, ToSchema)]
pub struct CreateDeal {
    #[garde(required, custom(not_blank), length(max = 255))]
    pub title: Option<String>,
    #[garde(required)]
    pub company_id: Option<Uuid>,
    #[garde(required, range(min = 0, max = MAX_AMOUNT_CENTS))]
    pub amount_cents: Option<i64>,
    // ...
}
```

An empty body `{}` now reports all three fields:

```json title="400 Bad Request"
{
  "statusCode": 400,
  "message": [
    "amount_cents should not be empty",
    "company_id should not be empty",
    "title should not be empty"
  ],
  "error": "Bad Request",
  "code": "validation_failed",
  "errors": {
    "amount_cents": ["amount_cents should not be empty"],
    "company_id": ["company_id should not be empty"],
    "title": ["title should not be empty"]
  }
}
```

## International input

People write their names in Arabic and Devanagari, type codes with the digits their keyboard has, and paste phone numbers in every format. navree-validation checks this input and turns it into one canonical form, so your handler compares and stores `+201001234567`, `EG` and `ar-EG`, not whatever was typed.

| Type or rule | Accepts | The handler gets | Message when it fails |
|---|---|---|---|
| `rules::unicode_name` | names in any script | the string as sent | `name must contain only letters, numbers, spaces, hyphens, apostrophes and periods` |
| `Folded<String>`, `Folded<u32>`, ... | digits of any script | ASCII digits, then the parsed value | the inner type's (`code must be longer than ...`) |
| `Phone` | phone numbers as people write them | E.164: `+447400123456` | `phone must be a valid phone number` |
| `Country` | ISO 3166-1 alpha-2 codes, any case | `GB` | `country must be a valid ISO 3166-1 alpha-2 country code` |
| `Locale` | BCP 47 language tags, any case | `en-GB` | `locale must be a valid locale` |
| `SafeHtml` | rich text | HTML with only the allowed tags | never fails on a string |

The types do their work while the extractor deserializes. A field typed `Phone` holds a valid number in E.164 form, and a bad number is a field error like a wrong type. `rules::phone`, `rules::country` and `rules::locale` check a `String` field instead and leave it as sent.

```rust title="src/contacts/dto.rs"
use garde::Validate;
use navree::validation::{rules, Country, Locale, Phone, SafeHtml};
use serde::Deserialize;

#[derive(Debug, Deserialize, Validate)]
pub struct CreateContact {
    #[garde(length(chars, min = 1, max = 100), custom(rules::unicode_name))]
    pub name: String,
    #[garde(skip)]
    pub phone: Phone,
    #[garde(skip)]
    pub mobile: Option<Phone>,
    #[garde(skip)]
    pub country: Country,
    #[garde(skip)]
    pub locale: Option<Locale>,
    #[garde(length(chars, max = 20_000))]
    pub notes: SafeHtml,
}
```

In an app whose default phone region is Egypt (`validation.phone.default_region = "EG"`), this body:

```json title="POST /api/v1/contacts"
{
  "name": "مُحَمَّد عبد الله",
  "phone": "٠١٠٠ ١٢٣ ٤٥٦٧",
  "country": "eg",
  "locale": "ar-eg",
  "notes": "<p dir=\"rtl\">عميل جديد</p><script>steal()</script>"
}
```

reaches the handler as:

```json title="CreateContact"
{
  "name": "مُحَمَّد عبد الله",
  "phone": "+201001234567",
  "mobile": null,
  "country": "EG",
  "locale": "ar-EG",
  "notes": "<p dir=\"rtl\">عميل جديد</p>"
}
```

The messages of these rules come from the [message table](#message-keys) like every other, with the keys `unicode_name`, `phone`, `country` and `locale`, so you can override them and [localization](https://navree.com/docs/packages/localization.md) translates them.

### Names in any script

`rules::unicode_name` accepts what names are made of in every script: letters, the combining marks many scripts write vowels and accents with (the Devanagari `ि`, the Arabic `َ`), numbers, spaces, hyphens, apostrophes (`'` and `’`), periods, the middle dot (`l·l`), and the zero-width non-joiner and joiner that Persian and Indic names are written with.

| Passes | Fails |
|---|---|
| `Amritpal Singh`, `अमृतपाल सिंह`, `ਅੰਮ੍ਰਿਤਪਾਲ ਸਿੰਘ` | `<script>alert(1)</script>`, `Robert'); DROP TABLE students;--` |
| `محمد عبد الله`, `مُحَمَّد`, `فاطمه‌زهرا` | `Ada 😀`, `@ada`, a name with a tab in it |
| `Zoë O’Brien-Smith`, `J. R. R. Tolkien`, `李小龙`, `Louis 14` | `   ` and `١٢٣`, which have no letter |

A name needs at least one letter and at most four combining marks in a row, which keeps stacked "Zalgo" text out. The rule says nothing about length: combine it with `length(chars, ...)`.

### Digits in any script

On Arabic keyboards the digits are `٠١٢٣٤٥٦٧٨٩`, on Persian ones `۰۱۲۳۴۵۶۷۸۹`, on Hindi ones `०१२३४५६७८९`. Wrap a field in `Folded` and its digits are ASCII before any rule sees them:

```rust title="src/auth/dto.rs"
use navree::validation::Folded;

#[derive(Debug, Deserialize, Validate)]
pub struct VerifyCode {
    #[garde(length(min = 6, max = 6), pattern(r"^[0-9]+$"))]
    pub code: Folded<String>, // "٦٥٤٣٢١" arrives as "654321"
    #[garde(range(min = 1, max = 99))]
    pub quantity: Option<Folded<u32>>, // "٤٢" and 42 are both 42
}
```

Without `Folded`, `"٦٥٤٣٢١"` is 12 bytes long and fails both the length and the pattern.

- `Folded<String>` (the default type parameter) keeps the folded text. garde's string rules (`length`, `pattern`, `ascii`, `contains`, ...) and Navree's rules work on it.
- `Folded<T>` parses the folded text into any `T: FromStr` (`u32`, `i64`, `f64`, `Decimal`) and accepts a JSON number too. `range` works on it, and a value that doesn't parse fails as `T` does (`quantity must be an integer number`).
- It derefs to the value (`*input.code`) and `into_inner()` hands it over. It serializes as the value.
- Every Unicode decimal digit folds: Arabic-Indic, Persian, Devanagari, Bengali, Gurmukhi, Thai, fullwidth and the rest. Other characters stay, including look-alikes that aren't decimal digits (`²`, `½`, `Ⅻ`).
- Query strings and forms fold too: `GET /codes?code=٦٥٤٣٢١` gives `654321`.

`fold_digits(text)` does the same to any string. It returns a `Cow<str>` that borrows `text` when there is nothing to fold.

### Phone numbers

(Feature: `validation-phone`) `Phone` holds a valid phone number in E.164 form, `+447400123456`: the form to store and to compare. It reads what people type: spaces, dashes and brackets, `tel:` URIs, an extension (`ext. 12`), and digits in any script.

| Input | Default region | `Phone` |
|---|---|---|
| `+44 7400 123456` | any | `+447400123456` |
| `+٩٧١ ٥٠ ١٢٣ ٤٥٦٧` | any | `+971501234567` |
| `+९१ ८१२३४ ५६७८९` | any | `+918123456789` |
| `07400 123456` | `GB` | `+447400123456` |
| `00 971 50 123 4567` | `GB` | `+971501234567` |
| `07400 123456` | none | `phone must be a valid phone number` |
| `+44 12` | any | `phone must be a valid phone number` |

Valid means the number fits its region's numbering plan (country code, length, prefixes), from libphonenumber's metadata, through the [rlibphonenumber](https://docs.rs/rlibphonenumber) crate. Nothing checks that the line is in service: send a code to it for that.

| Method | Returns |
|---|---|
| `as_str()`, `to_string()`, `String::from(phone)` | `+447400123456` |
| `country_code()` | `44` |
| `national_number()` | `7400123456` |
| `region()` | `Some(Country)` (`GB`); `None` for non-geographic numbers and areas outside ISO 3166-1, such as Kosovo |
| `international()`, `national()` | `+44 7400 123456`, `07400 123456`, plus ` x12` when there is an extension |
| `extension()` | `Some("12")` for `ext. 12` |

`Phone` serializes as the E.164 string. E.164 has no room for an extension, so read `extension()` before you store the number if you keep it. Outside a request, `Phone::parse(text, region)` parses with an explicit region and `"+44 7400 123456".parse::<Phone>()` parses international numbers.

To keep a field a `String` and only check it, write `#[garde(custom(rules::phone))]`. It reads numbers without a country code in the same default region.

#### The default region

A number without a country code, such as `07400 123456`, only means something in a region. `Phone` and `rules::phone` take the region from, in order:

1. the app's `RegionSource`, asked for the current request;
2. the `validation.phone.default_region` setting;
3. nowhere: the number is invalid.

Numbers written with a country code (`+44 ...`) ignore the region.

```toml title="config/app.toml"
[validation.phone]
default_region = "GB"
```

From the environment: `APP_VALIDATION__PHONE__DEFAULT_REGION=GB`. `ValidationModule::from_config()` reads it, and an unknown code fails the boot. In code, set `ValidationOptions { phone: PhoneOptions { default_region: Country::parse("GB").ok() }, ..ValidationOptions::default() }`.

When the region depends on the request (the workspace's country, the user's, the country your edge proxy detected), implement `RegionSource`. It gets the request head, `&mut Parts`, with its headers and its extensions (the principal and whatever earlier extractors cached), and returns `Option<Country>`. `None` falls back to the configured region.

```rust title="src/contacts/region.rs"
use navree::axum::http::request::Parts;
use navree::validation::{Country, RegionSource};
use navree::BoxFuture;
use navree::prelude::*;

/// The country the edge proxy reports in `CF-IPCountry`.
#[injectable]
pub struct EdgeCountry;

impl RegionSource for EdgeCountry {
    fn region<'a>(&'a self, parts: &'a mut Parts) -> BoxFuture<'a, Option<Country>> {
        let country = parts
            .headers
            .get("cf-ipcountry")
            .and_then(|value| value.to_str().ok())
            .and_then(|code| Country::parse(code).ok());
        Box::pin(async move { country })
    }
}
```

Register it from a module with `m.region_source::<EdgeCountry>()`, which makes it the app's `dyn RegionSource` (the method comes from `ModuleBuilderValidationExt`, which the prelude brings):

```rust title="src/contacts/module.rs"
use navree::prelude::*;

#[module(controllers = [ContactsController], register = Self::wire)]
pub struct ContactsModule;

impl ContactsModule {
    fn wire(&self, m: &mut ModuleBuilder) {
        m.region_source::<EdgeCountry>();
    }
}
```

The extractors ask the source only when a number needs a region, and at most once per request. Requests without such a number, and every route without a `Phone`, never call it.

### Countries and locales

`Country` is an ISO 3166-1 alpha-2 code: one of the 249 assigned codes, in upper case. `Locale` is a well-formed BCP 47 language tag in canonical case. Both read any case:

| Input | Becomes |
|---|---|
| `"gb"`, `"Gb"` | `Country` `GB` |
| `"uk"`, `"EU"`, `"XK"`, `"GBR"` | `country must be a valid ISO 3166-1 alpha-2 country code` |
| `"en-gb"`, `"EN-GB"` | `Locale` `en-GB` |
| `"zh-hant-tw"` | `Locale` `zh-Hant-TW` |
| `"ar-EG-u-nu-arab"` | `Locale` `ar-EG-u-nu-arab` |
| `"en_GB"`, `"e"`, `"en-"` | `locale must be a valid locale` |

- `Country` has `as_str()`, `Country::parse(code)` and `Country::all()`. Codes that are only reserved (`UK`, `EU`) or user-assigned (`XK`) are not countries here.
- `Locale` has `as_str()`, `language()` (`zh`), `script()` (`Some("Hant")`), `region()` (`Some("TW")`, or a UN M.49 area such as `419`) and `country()`. It checks the form of a tag, not whether the language exists, and subtags are separated by hyphens only.
- Both serialize as their canonical string. `rules::country` and `rules::locale` check `String` fields and leave them as sent.

### Rich text

(Feature: `validation-html`) `SafeHtml` holds rich text from the client after sanitizing it: the HTML is parsed (with html5ever, which follows the HTML standard's parsing rules) and written back with only the tags, attributes and URL schemes of the app's `HtmlPolicy`. Scripts, event handlers, `style` attributes and `javascript:` or `data:` links never reach your handler, so you can store the value and render it as HTML.

```rust title="src/comments/dto.rs"
use navree::validation::SafeHtml;

#[derive(Debug, Deserialize, Validate)]
pub struct NewComment {
    #[garde(length(chars, min = 1, max = 20_000))]
    pub body: SafeHtml,
}
```

With the default policy:

| Input | `SafeHtml` |
|---|---|
| `<p>Hello <strong>world</strong></p>` | unchanged |
| `<p dir="rtl" lang="ar">مرحبا</p>` | unchanged |
| `<a href="https://example.com">x</a>` | `<a href="https://example.com" rel="noopener noreferrer nofollow">x</a>` |
| `<script>alert(1)</script><p>after</p>` | `<p>after</p>` |
| `<img src=x onerror=alert(1)>` | empty |
| `<a href="javascript:alert(1)">x</a>` | `<a rel="noopener noreferrer nofollow">x</a>` |
| `<p onclick="steal()" style="color:red">x</p>` | `<p>x</p>` |
| `<!-- note --><u>hi</u>` | `<u>hi</u>` |
| `1 < 2 & 3` | `1 &lt; 2 &amp; 3` |

- A tag that isn't allowed is removed and its text kept. `script` and `style` are removed with their content.
- It never fails on a string, because anything unsafe is removed. Rules measure the sanitized HTML: `length(chars, max = 20_000)` limits what you store, and a body that was only a script fails `min = 1`.
- It serializes as the sanitized string, derefs to `str`, and `as_str()`, `into_inner()` and `String::from(html)` give it back. `SafeHtml::new(html)` sanitizes with the default policy, `SafeHtml::with_policy(html, &policy)` with another.

#### The HTML policy

The default policy keeps what a rich-text editor produces:

| Key | Default |
|---|---|
| `tags` | `a`, `b`, `blockquote`, `br`, `code`, `del`, `em`, `h1` to `h6`, `hr`, `i`, `li`, `ol`, `p`, `pre`, `s`, `span`, `strong`, `sub`, `sup`, `table`, `tbody`, `td`, `th`, `thead`, `tr`, `u`, `ul` |
| `generic_attributes` | `dir`, `lang`, allowed on every tag, so right-to-left text keeps its direction |
| `tag_attributes` | `a`: `href`, `title`; `ol`: `start`; `td`: `colspan`, `rowspan`; `th`: `colspan`, `rowspan`, `scope` |
| `url_schemes` | `http`, `https`, `mailto`, `tel`; relative URLs are kept |
| `link_rel` | `noopener noreferrer nofollow`, set on every link |

Images aren't in the default: an image in one user's comment loads from any server when another user reads it. Change the policy in the `validation.html` section. A key you set replaces its default:

```toml title="config/app.toml"
[validation.html]
tags = ["p", "br", "strong", "em", "a", "ul", "ol", "li", "img"]
url_schemes = ["https", "mailto"]

[validation.html.tag_attributes]
a = ["href", "title"]
img = ["src", "alt"]
```

In code, `HtmlPolicy` has builders: `HtmlPolicy::default().allow_tags(["img"]).allow_attributes("img", ["src", "alt"])`, `url_schemes([...])`, `link_rel("noopener")`, and `HtmlPolicy::text_only()`, which keeps no tags at all. Pass it as `ValidationOptions { html, ..ValidationOptions::default() }`.

A policy can't allow `script` or `style`, and can't allow a `rel` attribute while `link_rel` is set (set `link_rel = ""` to let links keep their own). `ValidationModule` refuses to boot with such a policy and names the key: ``validation.html.tags: `script` can't be allowed (scripts and styles are always removed)``.

### Outside the extractors

The extractors give `Phone` and `SafeHtml` the request's `InputContext`: its default region and the app's HTML policy. Anywhere else, such as a job deserializing its payload, they use the defaults: no default region and the default policy. To use the app's, inject `Arc<InputContext>` (exported by `ValidationModule`) and deserialize inside its `scope`:

```rust
let context = ctx.get::<InputContext>().await?;
let row: ImportRow = context.scope(|| serde_json::from_str(&line))?;
```

`InputContext::new().with_default_region(..).with_html(..)` builds one by hand, for tests or scripts.

`validate_in(&value, Some(&ctx))`, which checks a value that arrived some other way (a gRPC message, an event), runs its rules in the app's context as configured: `rules::phone` reads a number without a country code in `validation.phone.default_region`. There is no request, so no `RegionSource` is asked. `validate_value(json, Some(&ctx))` (MCP tool arguments, agent tool calls) and extractors built on `ValidInput` (navree-micro's `Payload<T>`) do the same while they deserialize: a `Phone` without a country code gets `validation.phone.default_region`, and `SafeHtml` the app's policy. `validate_json(&json, &messages)` takes no app, so it uses the defaults unless you call it inside `context.scope(..)`.

The extractors resolve the context only when a value asks for it: a phone number without a country code, or a `SafeHtml` field. The rest of your routes pay nothing for it.

### In the OpenAPI document

These types don't describe themselves to utoipa. Mark such a field with the schema of what it serializes as:

```rust
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreateContact {
    #[garde(skip)]
    #[schema(value_type = String)]
    pub phone: Phone,
}
```

## Error responses

Every validation failure is a `400 Bad Request` with `Content-Type: application/json`:

- `statusCode` is `400` and `error` is `"Bad Request"`.
- `message` is a list of **every** message, ordered by field path (alphabetically), then in the order the rules failed.
- `errors` maps each field path to its messages. Nested fields use dots and list indexes: `customer.address.city`, `items.1.quantity`.
- The request id is in the `X-Request-Id` header, not the body.

Problems with the input as a whole use the root name. A malformed JSON body gives:

```json title="400 Bad Request"
{
  "statusCode": 400,
  "message": ["body must be a valid JSON string"],
  "error": "Bad Request",
  "code": "validation_failed",
  "errors": { "body": ["body must be a valid JSON string"] }
}
```

Query strings work the same way. `GET /api/v1/contacts?search=a&per_page=500` gives:

```json title="400 Bad Request"
{
  "statusCode": 400,
  "message": [
    "per_page must not be greater than 100",
    "search must be longer than or equal to 2 characters"
  ],
  "error": "Bad Request",
  "code": "validation_failed",
  "errors": {
    "per_page": ["per_page must not be greater than 100"],
    "search": ["search must be longer than or equal to 2 characters"]
  }
}
```

The same format is used for every 400 with field errors, including the ones you [build yourself](#validating-by-hand) and the ones navree-auth returns. The [errors page](https://navree.com/docs/basics/errors.md) describes the error format as a whole.

## Custom messages

The built-in messages read as one short sentence: the field's path, then the rule, with no capital letter and no full stop. You can replace any of them, for every field or for one field, and you can give fields display names.

### Overriding messages

Message templates are keyed by rule (`required`, `min.string`) or by field and rule (`email.unique`). A field-specific template wins over a rule template, which wins over the built-in one:

```rust title="src/app.rs"
use std::collections::BTreeMap;

use navree::validation::ValidationOptions;

let options = ValidationOptions {
    messages: BTreeMap::from([
        ("required".to_owned(), "Please fill in :attribute.".to_owned()),
        ("email.unique".to_owned(), "This e-mail is already registered.".to_owned()),
    ]),
    attributes: BTreeMap::from([("first_name".to_owned(), "first name".to_owned())]),
    ..ValidationOptions::default()
};
let app = Navree::app().module(ValidationModule::with_options(options));
```

With these options a missing `first_name` reads `Please fill in first name.`. `ValidationOptions` also holds the [default phone region](#the-default-region) (`phone`) and the [HTML policy](#the-html-policy) (`html`), so start from `..ValidationOptions::default()`.

### From configuration

`ValidationModule::from_config()`, or a plain `.module(ValidationModule)`, reads the `validation` section, so you can keep messages out of your code:

```toml title="config/app.toml"
[validation.messages]
required = ":Attribute is required."
"email.unique" = "This e-mail is already registered."

# Nested tables flatten to dotted keys: this sets `min.string`.
[validation.messages.min]
string = ":Attribute needs at least :min characters."

[validation.attributes]
first_name = "first name"
"address.city" = "city"
```

The same section holds `[validation.phone]` and `[validation.html]`, described under [international input](#international-input).

`ValidationModule::for_root()` uses the built-in messages and ignores the section. Without `ValidationModule` in the app, the extractors use the built-in messages, no default phone region and the default HTML policy.

### Placeholders

| Placeholder | Value |
|---|---|
| `:attribute` | the field's display name: its path (`first_name`, `items.0.sku`) unless `attributes` names it |
| `:Attribute` | the same, first letter in upper case |
| `:min`, `:max` | the rule's bounds |
| `:other` | the other field of `matches` (shown by its display name) |
| `:values` | the allowed values (`in`), or the prefix or suffix |
| `:format`, `:pattern`, `:value` | the date format, the regular expression, the `contains` text |

Placeholders the rule doesn't fill are left as they are.

### Message keys

These are the keys you can override, with their built-in templates:

| Key | Template |
|---|---|
| `required` | `:attribute should not be empty` |
| `min.string`, `max.string` | `:attribute must be longer than or equal to :min characters`, `:attribute must be shorter than or equal to :max characters` |
| `min.array`, `max.array` | `:attribute must contain at least :min elements`, `:attribute must contain no more than :max elements` |
| `min.numeric`, `max.numeric` | `:attribute must not be less than :min`, `:attribute must not be greater than :max` |
| `email`, `url`, `ip`, `ipv4`, `ipv6`, `phone`, `credit_card` | `:attribute must be a valid email address`, `:attribute must be a URL address`, ... |
| `regex` | `:attribute must match :pattern regular expression` |
| `alpha_num`, `ascii` | `:attribute must contain only letters and numbers`, `:attribute must contain only ASCII characters` |
| `contains`, `starts_with`, `ends_with` | `:attribute must contain a :value string`, `:attribute must start with :values`, `:attribute must end with :values` |
| `same`, `confirmed` | `:attribute must match :other`, `:attribute confirmation does not match` |
| `in` | `:attribute must be one of the following values: :values` |
| `uuid`, `date_format`, `date` | `:attribute must be a UUID`, `:attribute must be a valid date in the format :format`, `:attribute must be a valid ISO 8601 date string` |
| `unicode_name` | `:attribute must contain only letters, numbers, spaces, hyphens, apostrophes and periods` |
| `country`, `locale` | `:attribute must be a valid ISO 3166-1 alpha-2 country code`, `:attribute must be a valid locale` |
| `string`, `integer`, `numeric`, `boolean`, `array`, `object`, `json` | `:attribute must be a string`, `:attribute must be an integer number`, ... |
| `prohibited` | `property :attribute should not exist` |
| `unique`, `exists` | `:attribute has already been taken`, `:attribute does not exist` |
| `invalid` | `:attribute is invalid` (used for any rule without a template) |

The full table is `navree::validation::DEFAULT_MESSAGES`.

> **Note**
>
> The built-in messages are English, with one table per app. [Localization](https://navree.com/docs/packages/localization.md) translates them per request, in the locale of the user, the workspace or `Accept-Language`, the international keys (`phone`, `country`, `locale`, `unicode_name`) included.

## Custom rules

A garde `custom` rule is a function taking the value and garde's context. Return `rule_error` with a rule key to get a message from the table, so it can be overridden like a built-in one. Or return `garde::Error::new` with your own text, which may use `:attribute`:

```rust title="src/teams/rules.rs"
use navree::validation::rule_error;

pub fn even(value: &u32, _ctx: &()) -> garde::Result {
    if value.is_multiple_of(2) {
        Ok(())
    } else {
        Err(rule_error("even", &[("value", value)]))
    }
}

pub fn not_blank(value: &str, _ctx: &()) -> garde::Result {
    if value.trim().is_empty() {
        Err(garde::Error::new(":attribute must not be blank"))
    } else {
        Ok(())
    }
}

#[derive(Debug, Deserialize, Validate)]
pub struct Teams {
    #[garde(custom(even))]
    pub size: u32,
    #[garde(custom(not_blank))]
    pub name: String,
}
```

`even` has no built-in template, so it renders the `invalid` one (`size is invalid`) until you add a message for the key `even`, for example `even = ":value is not an even number."` under `[validation.messages]`.

> **Note**
>
> The extractors need `Validate<Context = ()>`, so garde's `#[garde(context(...))]` isn't supported. Rules that need outside data, such as the database, are [async rules](#database-rules).

## Database rules

Some rules need the database: an email must not be taken yet, a `company_id` must point at a real company. Implement `AsyncRules` on the DTO and take `ValidAsync<...>` instead of `Valid<...>`:

```rust title="src/users/dto.rs"
use navree::validation::{AsyncRule, AsyncRules};

#[derive(Debug, Deserialize, Validate)]
pub struct UpdateUser {
    #[garde(email)]
    pub email: String,
    #[garde(skip)]
    pub team_id: Option<Uuid>,
}

impl AsyncRules for UpdateUser {
    fn async_rules(&self) -> Vec<AsyncRule> {
        vec![
            AsyncRule::unique("email", "users", "email", self.email.clone(), None)
                .ignore_path_param("id"),
            AsyncRule::exists("team_id", "teams", "id", self.team_id),
        ]
    }
}
```

```rust title="src/users/controller.rs"
#[controller("/users", version = "1")]
#[authenticated]
impl UsersController {
    #[patch("/{id}")]
    async fn update(
        &self,
        uow: Uow,
        Path(id): Path<Uuid>,
        ValidAsync(Json(input)): ValidAsync<Json<UpdateUser>>,
    ) -> Result<Json<UserResource>> {
        // `input` passed the garde rules, then both database rules.
        // ...
    }
}
```

A failed rule is a field error like any other: `email has already been taken`, `team_id does not exist`.

- `AsyncRule::unique(field, table, column, value, ignore_id)` passes when no row of `table` has `value` in `column`. When `ignore_id` is `Some(id)`, the row whose `id` column equals it doesn't count, which is what an update needs.
- `.ignore_path_param("id")` does the same with the route's `{id}` parameter, when it is a UUID. An explicit `ignore_id` wins.
- `AsyncRule::exists(field, table, column, value)` passes when some row has `value` in `column`.
- A `None` value passes without a query, so optional fields stay optional. Combine with `required` to demand a value.
- Bind values with the column's type: a `Uuid` for UUID columns, a `String` for text.

How they run:

- Only after every garde rule passes, so no query runs for input that is already invalid.
- Inside the request's [unit of work](https://navree.com/docs/database/unit-of-work.md), as `SELECT 1 FROM <table> WHERE <column> = ? [AND id <> ?] LIMIT 1` with bound values. The app needs `DatabaseModule`; without it, a rule that has a value to check answers 500.
- Table and column names must match `[a-z_][a-z0-9_]*`. Anything else fails with a 500 before any SQL runs, so a name can never inject SQL. Never build them from input.

Outside a request, check a rule yourself with `rule.passes(conn).await`, which returns `Result<bool, RuleError>`.

> **Warning: No extra conditions**
>
> A rule compares one column with one value. It can't add conditions such as `deleted_at IS NULL`, so `exists` accepts a soft-deleted row. Check such cases in your service, as the CRM does for deleted companies.

## Validating nested data

Mark a field holding another struct, or a list of structs, with `dive`. Use `inner(...)` to apply rules to each item of a list of plain values:

```rust title="src/orders/dto.rs"
#[derive(Debug, Deserialize, Validate)]
pub struct CreateOrder {
    #[garde(dive)]
    pub customer: Customer,
    #[garde(length(min = 1), dive)]
    pub items: Vec<OrderItem>,
}

#[derive(Debug, Deserialize, Validate)]
pub struct Customer {
    #[garde(email)]
    pub email: String,
    #[garde(dive)]
    pub address: Address,
}

#[derive(Debug, Deserialize, Validate)]
pub struct Address {
    #[garde(length(min = 1))]
    pub city: String,
}

#[derive(Debug, Deserialize, Validate)]
pub struct OrderItem {
    #[garde(length(min = 1))]
    pub sku: String,
    #[garde(range(min = 1, max = 99))]
    pub quantity: u32,
}
```

Errors name the full path, with list indexes:

```json title="400 Bad Request (errors)"
{
  "customer.address.city": ["customer.address.city must be longer than or equal to 1 characters"],
  "items.1.quantity": ["items.1.quantity must not be less than 1"],
  "items.1.sku": ["items.1.sku must be longer than or equal to 1 characters"]
}
```

serde reports nested type errors at their path too: `"quantity": "two"` in the first item gives `items.0.quantity must be an integer number`.

To override a nested field's message or name, use its full dotted path: `"customer.address.city" = "city"` under `[validation.attributes]`. List items have their index in the path, so an override for one specific index rarely makes sense; override the rule instead.

## Validating by hand

Some checks only make sense in a service: a date range, a rule that depends on the stored record. Collect them in `ValidationErrors` and return them as the same 400:

```rust
use navree::validation::ValidationErrors;

pub fn check_dates(starts_on: &str, ends_on: &str) -> Result<()> {
    let mut errors = ValidationErrors::new();
    if ends_on < starts_on {
        errors.add("ends_on", "ends_on must be after starts_on");
    }
    if !errors.is_empty() {
        return Err(errors.into_app_error());
    }
    Ok(())
}
```

- `add(field, message)` drops duplicates; `merge(other)` adds another set.
- `ValidationErrors` converts into `AppError` (`.into_app_error()` or `?`) and implements `IntoResponse`.
- To reuse the app's templates, inject `Arc<Messages>` (exported by `ValidationModule`) and call `messages.render("company_id", "exists", &[])`. The CRM does this to report a soft-deleted company as `company_id does not exist`.
- `AppError::validation(map)` builds the same response from a `BTreeMap<String, Vec<String>>`.

Calling garde's `input.validate()` yourself works anywhere, but it returns garde's own report and messages. For Navree's messages on a value that didn't come through an extractor (a gRPC message, an event payload), call `validate_in(&value, Some(&ctx)).await` with the app's `AppContext` (its messages and its [input context](#outside-the-extractors); `None` uses the built-in ones), or `validate(&value, None, &messages, "input")` with a `Messages` you hold. Both return `ValidationErrors`, keyed by dotted field path.

## Testing

`TestResponse` has assertions for this format. `assert_validation_error(field)` checks for a 400 with at least one message for the field, and `assert_error(400, message)` checks that the `message` list contains that exact message:

```rust title="tests/contacts.rs"
client
    .post("/api/v1/contacts")
    .json(&json!({
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "nope",
        "tags": [],
        "status": "lead"
    }))
    .send()
    .await
    .assert_validation_error("email")
    .assert_error(400, "email must be a valid email address");
```

See [HTTP tests](https://navree.com/docs/testing/http-tests.md) for the test client.

- [Errors](https://navree.com/docs/basics/errors.md): The JSON error shape, `AppError` and mapping your own errors.
- [Requests and extractors](https://navree.com/docs/basics/requests.md): Path parameters, query strings, bodies and the current user.
- [The unit of work](https://navree.com/docs/database/unit-of-work.md): The per-request transaction async rules run in.
- [OpenAPI](https://navree.com/docs/digging-deeper/openapi.md): Describe your DTOs in the generated API document.
