# Validation & Data Import

> Constraints compile into a state machine — the same gates validate a form keystroke and an imported spreadsheet row, and rastack import loads CSV, Excel and PDF data through them.

The generator's constraints — requiredness, datatype, `maxLength`, enum membership, `unique`, foreign keys — are not a bag of ad-hoc checks. They compile into a **deterministic finite state machine** (`rastack/validate`): each constraint becomes one gate, and a value must pass every gate, in a fixed order, to reach the accepting state.

```text title="the constraint state machine"
pristine → present → typed → bounded → member → unique → resolved → valid
              └─────────┴───── any gate fails ────┴─────────┘
                                    ↓
                                 invalid
```

Compiling **builds the constraints**: a gate is only emitted when its constraint exists, so an unconstrained optional string's machine is just `pristine → present → typed → valid`. Running walks the gates with a raw value, **coercing as it goes** (`"12"` → `12`, `"yes"` → `true`), and halts at the first failing guard — reporting the state reached and the exact constraint that rejected the value.

## The gates

| Gate | Constraint | Emitted when |
| --- | --- | --- |
| `present` | required / optional | always (first gate) |
| `typed` | datatype + string `format` | always — also coerces the value |
| `bounded` | `maxLength` | the field has a `maxLength` |
| `member` | enum membership | the schema carries an `enum` |
| `unique` | no duplicate in the dataset | the field is `unique` |
| `resolved` | the FK target exists | the field is a foreign key |

> **Note:** `unique` and `resolved` are **dataset-level** gates: they only engage when the run is given a dataset context (a seen-set, a relation resolver). A form validating a single value passes through them untouched — mirroring the server, where uniqueness and FK existence are checked against the table, not the payload.

## In the form hooks

Every form field carries its machine. `getFormField(name)` compiles it from the same write JSON Schema the form already validates against, and runs it against the current value on every render:

```tsx title="app/airports.tsx"
const form = useUpdateAirportForm({ formValues: { code: "" } });
const field = form.getFormField("code");

field.constraints;         // ["required", "string", "maxLength ≤ 3", "unique"]
field.validation?.state;   // "valid" | "invalid" for the current value
field.validation?.path;    // ["pristine", "present", "typed", "invalid"]
field.validation?.failed;  // { gate: "bounded", constraint: "maxLength ≤ 3", … }
```

So a form can render exactly which gate the value stops at — with the same semantics the API enforces on write, because both compile from the same constraints. The existing validation path is unchanged; the machine is purely additive.

## Import external data

`rastack import` loads **CSV, Excel (XLSX) and PDF** files into the shape of a compiled resource and runs every row through the resource's machine — with the full dataset context, so in-file duplicates and dangling foreign keys are rejected with row-located errors.

```bash title="terminal"
# validate a CSV against a resource's constraints
rastack import airports.csv --resource airports.airport

# resolve foreign keys against a related file's ids
rastack import terminals.xlsx --resource airports.terminal \
    --related airports.airport=airports.csv

# write the coerced valid records for seeding or POSTing
rastack import bookings.pdf --resource airports.booking --out clean.json
```

Source columns map onto fields by normalised name — `"Airport Code"`, `airport_code` and `airportCode` all match — and every failure names its row, field and gate:

```text title="rastack import output"
  code → code
  name → name

  ✗ row 2  code: "LHRX" is longer than 3 characters  [failed at "bounded" — maxLength ≤ 3]
  ✗ row 3  code: duplicate value "LHR"               [failed at "unique" — unique]
  ✗ row 4  code: code is required                    [failed at "present" — required]

✗ 1/4 row(s) valid, 3 rejected
```

The exit status is non-zero when any row fails (so CI can gate on it) unless `--allow-invalid` is passed; `--report report.json` writes the full validation report.

## Source formats

| Format | How it's read |
| --- | --- |
| `.csv` | the standard CSV parser (quoted fields supported) |
| `.xlsx` | a dependency-free reader: shared + inline strings, numbers, cached formula results from the first worksheet |
| `.pdf` | a best-effort table extractor: decodes the content streams and clusters positioned text into rows |

> **Warning:** PDFs carry no table structure — only positioned text — so the PDF reader targets machine-generated tabular reports. Merged cells, wrapped cell text and scanned pages won't survive; export those sources to CSV or XLSX instead. Every extracted value still runs through the machine, so a misread cell is **rejected with a located error**, never silently imported.

> **Tip:** The machine compiles from the manifest, so run `rastack compile` first (or point `--schema` at a `schema.rastack.json`). Command details on the [CLI Reference](https://reactapistack.com/docs/cli.md) page.

---

_Source: [https://reactapistack.com/docs/validation](https://reactapistack.com/docs/validation) · Full docs as one file: [llms-full.txt](https://reactapistack.com/llms-full.txt)_
