Validation
Rules on the model, errors keyed by column
# Validation
Rules live on the model.
```pascal
procedure TCustomer.Rules(V: TValidator);
begin
V.Field('Name').Required.MaxLen(120);
V.Field('Email').Required.Email.UniqueIn('customers');
V.Field('Balance').Min(0);
V.Field('Age').Between(18, 120);
V.Field('Status').OneOf(['draft', 'live', 'archived']);
V.Field('PasswordConfirmation').SameAs('Password');
end;
```
```pascal
Req.FillInto(C);
if not C.Validate then
Exit(BackWithErrors(C.Errors));
C.Save;
```
`Validate` returns False and leaves the errors in `C.Errors`.
## Rules
| | |
|---|---|
| `Required` | Present and not blank |
| `MinLen(n)`, `MaxLen(n)` | String length |
| `Min(v)`, `Max(v)`, `Between(lo, hi)` | Numeric, as `Currency` |
| `Email` | Shape, not deliverability |
| `OneOf([...])` | One of a set |
| `SameAs(prop)` | Equal to another field — password and confirmation |
| `UniqueIn(table[, column])` | No other row has this value |
| `Unique` | `UniqueIn` on the model's own table and column |
| `Exists(table[, column])` | The value is a row in `table` — a reference that points at something; `column` is `id` unless you say |
| `Says(message)` | Overrides the message of the rule immediately before it |
The messages are keys — `validation.required` and the rest — so they can be
said in the reader's language, and a field can have a name there other than
its column: see [Languages](lang.md). With no lang files they are the
English in this table's rules, word for word.
Each returns the chain, so they compose:
```pascal
V.Field('Email').Required.Says('We need an email address.').Email;
```
They stop at the first failure per field: one error per field, not five.
`Exists` passes a blank value — 0 for a number, `''` for text — because
refusing one is `Required`'s job; a nullable reference is checked only when
it is set. A soft-deleted row still counts as a row.
`UniqueIn` uses the ambient connection and **skips the row itself** when the
model is already persisted — otherwise updating a record would always report
its own email as taken. The column defaults to the one the property maps to.
## Fields and columns
> **Errors are keyed on the column name; rules are written with the property
> name.**
```pascal
V.Field('CustomerId'); { the property }
C.Errors.Has('customer_id'); { the column }
```
That is deliberate. The frontend receives errors keyed the way the form
fields are named, which follows the column, while the rules read like the
Pascal they are written in.
## The errors
```pascal
C.Errors.Count;
C.Errors.IsEmpty;
C.Errors.Has('email');
C.Errors.First('email'); { first message, or empty }
C.Errors.Field(I); { iterate }
C.Errors.Message(I);
C.Errors.WriteJson(W); { field -> message, the shape Inertia wants }
```
Fields without errors are not listed at all. `TErrors` lives in the arena
and dies with the request.
Messages are English, because everything a user of the framework sees is:
`:attribute is required`, `:attribute must be at least :min characters`, and so on.
## Across a redirect
Validation errors must survive the redirect that follows a failed POST.
They travel as **session flash**:
```pascal
Result := BackWithErrors(C.Errors);
```
The session's flash is **two maps**: what can be read now, and what is being
written for the next request. One map would give either a flash that never
disappears or one that cannot be read. See [Sessions](sessions.md).
> Inertia's own flash is thread-local and applies to **the response being
> built now**. It does not survive a redirect — the two requests can land on
> different workers, and without sessions there is no shared storage. Render
> the page directly instead of redirecting to it.
## To a client that is not a browser
A redirect with a flash is a browser mechanism end to end: it needs
somewhere to keep the errors between two requests, and a client that
follows the redirect and then reads the page it lands on. A program does
neither.
So the same call answers differently when the caller asked for JSON:
```pascal
Result := BackWithErrors(C.Errors); { 302 + flash, or 422 + problem+json }
```
```json
{
"type": "about:blank",
"title": "Unprocessable Content",
"status": 422,
"detail": "The request body did not validate.",
"errors": { "display_name": "display_name is required" }
}
```
The `errors` member is the same object Inertia gets as `props.errors`,
keyed the same way — on the **column** name. `ValidationProblem(C.Errors)`
in `Askr.Urd.Bind` builds it directly, for a handler that has no session
to flash into. See [APIs](api.md).
## Why it lives in the model unit
`TValidator` is declared in `Askr.Urd.Model`, not in a unit of its own.
`TModel.Rules` needs `TValidator`, and `TValidator` needs `TModel`: circular
between units, and Pascal does not allow it. The merge is the price.