# @wrnexus/validation > One fluent schema, validated on the server (API bodies, env vars) and mirrored to an eval-free browser validator for forms. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Boundary contracts Use `ContractRegistry` with `defineContract` or `defineEvent` to publish the same schema descriptors for APIs, actions, webhooks, realtime, queues, cron, pub/sub, plugins, configuration, and environment variables. ```ts import { ContractRegistry, defineEvent, v } from "@wrnexus/validation"; export const contracts = new ContractRegistry().register( defineEvent({ name: "user.created", version: 1, consumers: ["notification-worker", "audit-service"], payload: v.object({ userId: v.string().uuid(), createdAt: v.string().date() }), }), ); ``` Export the registry from `app/contracts.ts`, then accept a baseline with `wrnexus contracts snapshot`. CI can run `wrnexus contracts check`; removed contracts/fields, required-field additions, type changes, narrowed enums, and tighter validation fail with stable `WRN-CONTRACT-*` diagnostics and list known consumers. A generated `wrnexus.contracts.json` can be used instead of a module. ## Overview Define a schema once with the fluent `v` builder, then reuse it in three places: `.parse()` runs server-side and returns coerced values plus per-field errors; `.describe()` emits a plain-JSON `SchemaDescriptor` that the browser runtime interprets (no `eval`, no bundled validator); and helpers like `parseBody` and `parseEnv` wrn schemas straight into API routes and startup config. The server rule logic (`applyRule`/`checkField`) and the client runtime (`VALIDATE_RUNTIME`) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in `app/schemas/`. ## Installation ```bash bun add @wrnexus/validation ``` > Private package — the machine must be authenticated to the `wrnexus` npm org > (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported). ## API ### The `v` builder ```ts import { v } from "@wrnexus/validation"; ``` | Factory | Returns | Field methods | | ------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------- | | `v.string()` | `StringSchema` | `email()`, `url()`, `uuid()`, `date()`, `length(n)`, `oneOf(string[])`, `pattern(re)`, `trim()`, `min(n)`, `max(n)` | | `v.number()` | `NumberSchema` | `integer()`, `positive()`, `oneOf(number[])`, `min(n)`, `max(n)` | | `v.boolean()` | `BooleanSchema` | (base methods only) | | `v.object(fields)` | `ObjectSchema` | `parse(input)`, `describe()` | Every field schema is chainable and shares these base methods: - `min(n, message?)` / `max(n, message?)` — for strings, bounds the length; for numbers, bounds the value. - `required(message?)` — require a non-empty value and optionally replace the default `"Required"` message on both server and browser validation. - `optional()` — an empty/missing value passes instead of erroring `"Required"`. - `label(text)` — human label carried into the descriptor. - `default(value)` — value substituted when the field is absent (implies `optional`). - `refine(fn, message?)` — **server-only** predicate. `fn` returns `true` (ok), `false` (use `message`), or a `string` (that error). Not serialized to the client. Each string rule accepts an optional trailing `message` to override the default error text. ### `ObjectSchema` ```ts schema.parse(input: unknown): ParseResult schema.describe(): SchemaDescriptor ``` `parse` coerces each field (strings stay strings, `v.number()` runs `Number()`, `v.boolean()` treats `true` / `"true"` / `"on"` as true), applies its rules and refinements, fills in `default()` values, and returns: ```ts interface ParseResult> { ok: boolean; // true when errors is empty value: T; // coerced values (present pass or fail) errors: Record; // field name → first failing message } ``` `describe()` returns the JSON bridge for the client: ```ts interface SchemaDescriptor { type: "object"; fields: Record; } interface FieldDescriptor { type: "string" | "number" | "boolean"; optional?: boolean; label?: string; trim?: boolean; // strings only rules: RuleDescriptor[]; } ``` ### Rules and coercion `RuleDescriptor` is a discriminated union of the serializable rules — `min`, `max`, `length`, `email`, `url`, `uuid`, `date`, `oneOf`, `pattern`, `integer`. Two exported functions apply them and are shared by the server (the client runtime reimplements the same logic): - `applyRule(type, rule, value): string | null` — validate one already-coerced value against one rule. - `checkField(desc, raw): { value, error }` — coerce and validate one field. Empty input (`undefined`/`null`/`""`) is `"Required"` unless `optional`. Strings with `trim` are trimmed first. Numbers that fail `Number()` yield `"Must be a number"`. Notes on specific rules: `email`/`url`/`uuid` test built-in regexes; `date` uses `Date.parse`; `pattern` reconstructs a `RegExp` from its `source`/`flags` and passes silently if the pattern is invalid; `integer` requires `Number.isInteger`; `positive()` is implemented as `min(Number.MIN_VALUE)`. ### API helpers ```ts invalid(errors: Record): Response // ready 400 { ok:false, errors } parseBody(schema, req): Promise<{ ok: true; value: T } | { ok: false; response: Response }> ``` `parseBody` reads the request body from JSON, `application/x-www-form-urlencoded`, or `multipart/form-data`, validates it, and on failure hands back a ready 400 `Response`. ### Environment config ```ts parseEnv(schema: ObjectSchema, source?): T ``` Validates env vars (from `Bun.env`, falling back to `process.env`) against a schema and coerces them (`PORT` → number, `DEBUG` → boolean). On any problem it throws **one** error listing every offending variable, so misconfiguration fails fast at startup. ### Client runtime (from `runtime.ts`) ```ts renderSchemasScript(descriptors: Record): string VALIDATE_RUNTIME: string ``` - `renderSchemasScript` produces `window.__wrnSchemas = { name: descriptor, … };` to inline in the page. - `VALIDATE_RUNTIME` is a self-contained, eval-free IIFE string. Injected as a ` `; // render a
with [data-error="email"] etc. ``` ## Requirements / Notes - **Bun-only.** `parseEnv` reads `Bun.env` (falling back to `process.env`); `parseBody` and `invalid` use the Web `Request`/`Response` APIs that back `Bun.serve`. - Refinements (`refine`) run only server-side and are never serialized — client and server agree on every other rule because both interpret the same `RuleDescriptor` list. - No runtime dependencies. Ships as TypeScript source (`src/index.ts`) executed directly by Bun. - Pairs with the WrNexus server (`@wrnexus/core`) for route handlers and the SSR layer that injects `renderSchemasScript` / `VALIDATE_RUNTIME`. ## Helper and component kit The public helper API includes `parseOrThrow`, `ValidationError`, `validationResponse`, `firstValidationError`, `validationSummary`, and `schemaFieldNames`. Schema output is inferred automatically by `ObjectSchema`, `parseOrThrow`, `parseBody`, `parseEnv`, and `asyncSchema`. Use `InferSchema` when a named output type is useful: ```ts const accountSchema = v.object({ email: v.string().email(), attempts: v.number().integer(), }); type AccountInput = InferSchema; const account = parseOrThrow(accountSchema, input); // account.email: string // account.attempts: number ``` Enable `validationPlugin()` for: - `` - `` The summary block composes `Alert` from `@wrnexus/ui`, while `FieldError` remains a lightweight accessible field-level primitive. Schemas can drive external contracts without maintaining a second definition: ```ts import { localizeDescriptor, openApiRequestBody, parseDescriptor, toJsonSchema, } from "@wrnexus/validation"; const jsonSchema = toJsonSchema(contactSchema, { id: "urn:example:contact", title: "Contact request", }); const requestBody = openApiRequestBody(contactSchema); const mr = localizeDescriptor(contactSchema, (key, params) => translations.t(`validation.${key}`, params), ); const result = parseDescriptor(mr, input); ``` JSON Schema output targets draft 2020-12, closes unknown object properties, and maps lengths/ranges/formats/enums/patterns/integer rules. OpenAPI request bodies reuse the same properties. Localized descriptors preserve explicit custom messages and fill default required, type-coercion, and rule messages; the same descriptor is consumable by server parsing and the eval-free browser runtime.