# Validation

> You declare a field's rules once, on the entity — `[Required]`, `[Pattern]`, `[MaxLength]` — and give each rule the sentence to show when it fails. A bound `Input` then configures itself from them, the browser marks a bad value invalid, a save that cannot succeed is refused before it leaves the page, and a save the server refuses raises a `ValidationException` your action can catch and place beside the offending field.

<!-- id: ui-validation · area: ui · stability: stable · html: https://osysharp.com/reference/ui/validation/ -->

## Summary        {#summary}
The rules live on the **entity**, once, together with the words to say when they're broken:

```osy syntax
entity Organization {
  [Required("A name is required."), MaxLength(255)] string Name;
  [Required("A slug is required."),
   MaxLength(100),
   Unique,
   Pattern("^[a-z0-9]+(-[a-z0-9]+)*$", "Use lowercase letters, numbers and hyphens — like acme-corp.")]
  string Slug;
}
```

Everything else follows from that. The form doesn't restate the rule, so it can't drift from it.

## Signature      {#signature}
```osy syntax
[Required("message")]                  // …the message is optional on every rule
[Pattern("regex", "message")]
[MaxLength(100, "message")]
[MinLength(3, "message")]
[Min(0, "message")] [Max(10, "message")]

catch (ValidationException ex) {
  ex.Message      // the sentence(s) — what to show if you only want one line
  ex.Violations   // one Violation per refused field: .Entity .Field .Rule .Message
}

Validation.MessageFor(order.Customer)   // the sentence to show under ONE field, "" when it has none
Validation.MessageFor(value)            // …inside a control: its own Binding<T> parameter
```

## Description    {#description}
Four things happen. With the kit's field controls you author none of them; with your own controls, only the last.

**1. The control obeys the model.** `Input(value: draft.Slug)` already names the property it edits, so it configures
itself from that property's rules — `required`, `maxlength`, `minlength`, `pattern`, `min`, `max` become real
attributes on the field. The browser enforces what it can (you cannot type past a `maxlength`) and judges the rest, so
a bad value is genuinely invalid, not "invalid according to some code we wrote twice".

**2. Your design system paints it.** A field that fails its rules is in the `Invalid` state — an ordinary state in a
`variants` block, next to `Hover` and `Focus` — so you style it **once** and every control agrees:

```osy syntax
variants { base { … Invalid { Border = FillDanger; } } }
```
It holds until the user has actually touched the field, so a blank required field is not red before it's been filled in.

**3. A doomed save never leaves the page.** `UnitOfWork.Commit()` checks the pending changes against the model first. If they
can't be accepted, it raises straight away — no round-trip, and the message appears instantly.

**4. A refused save says why — where it happened.** Whether it was refused locally or by the server, it raises the same
`ValidationException`, carrying the same `Violations`. **The kit's field controls place each message themselves:**
`Field`, `TextAreaField`, `NumberField`, `DecimalField` and the pickers show, under themselves, the sentence for the
field they edit — a save held for a broken rule and a save the server refused alike — so a page that uses them writes
nothing at all (the first example below). Writing your own control, or a bare `Input`? Read `Validation.MessageFor(field)`: it
answers the sentence the person has been shown for that one field, and `""` when there is none. A rule with no message of its own still says it in the form's words — `[Max(2100)] int YearFounded` reads *"Year
founded must be at most 2100."*, using the field's `[Label]` when it has one ([[entity-constraints#the-message]]).
That sentence reaches a signed-out visitor too, like a message you wrote; any other sentence the platform makes names
the entity, so a signed-out visitor is told only *"One or more values are invalid."* in its place. And the refused save
keeps the person's edits: their value stays in the field, with the sentence under it ([[ui-page-unit-of-work#held-save]]).

**The client is being polite, not standing guard.** The server validates every write it receives, and it has the last
word — a slug someone else claims between your check and your save is refused *there*. The local check is deliberately
conservative: it never accuses a field the user hasn't filled in (that field may have a default only the server knows),
and it never tries to answer `Unique`, which is a question about *other* rows. When in doubt it stays quiet and lets
the write go, because wrongly blocking a good save is worse than a late "no".

**The platform never writes the words.** It reports which field broke which rule and hands you the sentence *you*
declared beside it. A regex is not something to show a human, so if you word nothing, nothing is worded.

### Where does the draft live?   {#draft-scope}
Everything above rests on the draft being an **entity**: `Input(value: draft.Slug)` can configure itself from
`Slug`'s rules only because `draft` is an `Organization`, and `[Required("A slug is required.")]` is the only place
that sentence is written. Hold the fields as loose `string`s instead and both halves go: the control has no property
to read rules from, and each sentence has to be re-typed as a guard in the action — the same words in two files, free
to drift. So **keep the entity-typed draft**.

There is exactly one shape where it bites, and it is worth knowing before you meet it: **a component that holds a
draft AND queries the same entity**. A draft is pending from the moment the component mounts, and the page's own
queries read pending rows back — so the list paints a blank phantom row, and the never-filled draft rides the next
genuine save, failing on a `[Required]` for a row the user never opened. `osy lint` reports that pair as
`ui-draft-field-ghosts-its-own-list` (SHOULD) and names both members.

**The fix that keeps this page's guarantee** is not to give up the entity — it is to give the draft its own unit of
work, by putting it on a component you open with `Dialog.Open(…, unitOfWork: Root)`. Nothing is then pending in the
list's unit of work, and the rules and their sentences stay declared exactly once:

```osy title="the draft on its OWN unit of work — the list stays clean, the messages stay declared once" test app=ui-validation-scope
entity Organization {
  [Required("A name is required."), MaxLength(255)] string Name;
}

[Render(CSR)]
component NewOrganization() {
  Organization draft = new Organization {};      // pending in THIS component's unit of work, not the page's

  action Save()   { Dialog.Confirm(); }          // a Root dialog's Confirm is what persists it
  action Cancel() { Dialog.Discard(); }

  render {
    Stack(gap: 2) {
      Input(value: draft.Name, placeholder: "Organization name");
      Button("Cancel", onPress: Cancel);
      Button("Save", onPress: Save);
    }
  }
}

[Route("/orgs")]
[Render(CSR)]
component OrgListPage() {
  live var orgs = Organization.ToList();         // no draft here, so no phantom row
  action New() { Dialog.Open(NewOrganization(), unitOfWork: Root); }
  render {
    Stack(gap: 2) {
      foreach (var o in orgs) { Text(o.Name); }
      Button("New organization…", onPress: New);
    }
  }
}
```

Local scalars are the other way out the linter offers, and they are the right answer when the form's fields do not
correspond to one entity at all. Reach for them knowing the cost: **the declared sentence is gone**, and you write
`if (title == "") { formError = "A name is required."; return; }` for each rule you had declared. See
[[ui-data-mutation#draft-scope]].

## Examples       {#examples}
The everyday form — the rules on the entity, kit fields and a save control, and **no validation code in the page**.
A save held for `Name`'s rule shows *"A name is required."* under Name; a save the server refuses because the slug is
taken shows *"That slug is taken."* under Slug, a rule only the server can answer:

```osy title="kit fields place their own messages" test app=ui-validation-kit
using Osysharp.Ui;

entity Organization {
  [Required, MinLength(1, "A name is required."), MaxLength(255)] string Name;
  [Required, Unique("That slug is taken."), MaxLength(100)] string Slug;
}

[Route("/org/{id}")]
[Render(CSR)]
component OrgPage(Guid id) {
  live var org = Organization.FirstOrDefault(o => o.Id == id);
  render {
    if (org != null) {
      Stack(gap: 4) {
        PageHead(org.Name) { SaveActions(saved: "Saved."); }
        Field("Name", value: org.Name);
        Field("Slug", value: org.Slug);
      }
    }
  }
}
```

A control of your own reads the same answer for the field it was handed — its `Binding<T>` parameter names the
caller's field, so this works for any row of any entity:

```osy title="a control of your own shows the message for its field" test app=ui-validation-own-control
entity Organization {
  [Required, MinLength(1, "A name is required.")] string Name;
}

component NameBox(Binding<string> value) {
  render {
    Stack(gap: 1) {
      Input(value: value);
      Text(Validation.MessageFor(value));
    }
  }
}

[Route("/org/{id}/name")]
[Render(CSR)]
component OrgName(Guid id) {
  live var org = Organization.FirstOrDefault(o => o.Id == id);
  render { if (org != null) { NameBox(value: org.Name); } }
}
```

Placing the messages yourself, from the exception — the whole loop by hand, rules on the entity and messages beside
the fields that broke them. This page holds a draft and
**no query over `Organization`**, which is the shape the linter is looking for the absence of:

```osy title="org-form" test app=ui-validation
entity Organization {
  [Required("A name is required."), MaxLength(255)] string Name;
  [Required("A slug is required."),
   Pattern("^[a-z0-9]+(-[a-z0-9]+)*$", "Use lowercase letters, numbers and hyphens — like acme-corp.")]
  string Slug;
}

[Route("/org/new")]
[Render(CSR)]
component OrgCreatePage() {
  Organization draft = new Organization {};   // the draft IS the field — nothing to fetch at mount
  string nameError = "";
  string slugError = "";

  action Create() {
    nameError = "";
    slugError = "";
    try {
      UnitOfWork.Commit();
      Navigation.Close("/org/new", true);
    }
    catch (ValidationException ex) {
      foreach (var v in ex.Violations) {
        if (v.Field == "Name") { nameError = v.Message; }
        else if (v.Field == "Slug") { slugError = v.Message; }
      }
    }
  }

  render {
    Stack(gap: 4) {
      Input(value: draft.Name, placeholder: "Organization name");
      if (nameError != "") { Text(nameError); }

      Input(value: draft.Slug, placeholder: "team-slug");
      if (slugError != "") { Text(slugError); }

      Button("Create", onPress: Create);
    }
  }
}
```

Only need one line, not per-field placement? `ex.Message` is the sentences, joined:

```osy title="one line instead of per-field placement" syntax
action Save() {
  try { UnitOfWork.Commit(); }
  catch (ValidationException ex) { error = ex.Message; }
}
```

## See also       {#see-also}
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — `UnitOfWork.Commit()`, binding an input to an entity field, and where a draft belongs.
- [Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard](https://osysharp.com/reference/ui/dialogs/) — `Dialog.Open(…, unitOfWork: Root)`, the component whose unit of work is its own.
- [debounce](https://osysharp.com/reference/ui/debounce/) — asking the server a question as you type (the is-this-name-taken check), which `Unique` cannot answer locally.
- [style props](https://osysharp.com/reference/ui/styling/) — `variants`, and the states a control can be in.
- [Navigation](https://osysharp.com/reference/ui/navigation/) — `Navigation.Save`, which can be refused the same way and caught the same way.
