# 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
}
```

## Description    {#description}
Four things happen, and you author only the last one.

**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`. Catch it and put each message beside the control that produced
it.

**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);
    }
  }
}

[Page("/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 whole loop — rules on the entity, 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;
}

[Page("/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.
