Files
WRNexusJS/packages/validation
ClintchizandClaude Opus 5 7a2b58652a
Quality / quality (ubuntu-latest) (push) Failing after 11m2s
Quality / quality (windows-latest) (push) Canceled after 0s
chore(release): prepare 0.8.6
Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6,
and rebuilds the editor compiler, language server and extension bundles that
embed the version.

The release carries the output delivery fix: camelCase outputs now reach
parent bindings, and 18 components emit through output.* instead of
hand-built CustomEvents. See the 0.8.6 migration entry for what changes for
consumers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:54:44 +05:30
..
2026-08-03 19:47:30 +05:30
2026-08-02 23:18:51 +05:30
2026-08-02 23:18:51 +05:30
2026-08-09 01:54:44 +05:30
2026-08-02 23:18:51 +05:30

@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.

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 wire 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

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

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

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:

interface ParseResult<T = Record<string, unknown>> {
  ok: boolean; // true when errors is empty
  value: T; // coerced values (present pass or fail)
  errors: Record<string, string>; // field name → first failing message
}

describe() returns the JSON bridge for the client:

interface SchemaDescriptor {
  type: "object";
  fields: Record<string, FieldDescriptor>;
}
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

invalid(errors: Record<string, string>): Response   // ready 400 { ok:false, errors }

parseBody<T>(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

parseEnv<T>(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)

renderSchemasScript(descriptors: Record<string, SchemaDescriptor>): string
VALIDATE_RUNTIME: string
  • renderSchemasScript produces window.__wireSchemas = { name: descriptor, … }; to inline in the page.
  • VALIDATE_RUNTIME is a self-contained, eval-free IIFE string. Injected as a <script>, it binds every form[data-schema] and validates on submit and blur, writing messages into [data-error="<field>"] elements and toggling aria-invalid / .wire-invalid. On a valid submit it fetches the form action as JSON (attaching the wire-csrf cookie as an x-csrf-token header), then follows data-redirect / a redirect in the response, surfaces server-side field errors, and fires wire:success / wire:error events. It exposes window.__wireValidate.init(root) and self-initializes on DOMContentLoaded.

Usage

Define a schema and validate an API body:

import { v, parseBody } from "@wrnexus/validation";

export const signupSchema = v.object({
  email: v.string().required("Enter your email address").trim().email(),
  password: v.string().required("Enter your password").min(8).max(200),
  age: v.number().integer().min(13).max(120).optional(),
  role: v.string().oneOf(["user", "admin"]).default("user"),
  agree: v.boolean(),
});

// inside a route handler
const result = await parseBody(signupSchema, req);
if (!result.ok) return result.response; // ready 400 with field errors
const { email, password, role } = result.value;

Server-only refinement:

const schema = v.object({
  username: v
    .string()
    .min(3)
    .refine((name) => !RESERVED.has(String(name)), "That name is taken"),
});

Validate environment at startup:

import { v, parseEnv } from "@wrnexus/validation";

export const env = parseEnv(
  v.object({
    DATABASE_URL: v.string().min(1),
    PORT: v.number().integer().default(3000),
    DEBUG: v.boolean().optional(),
  }),
);
// throws one readable error listing every bad variable if misconfigured

Wire the same schema into the browser:

import { renderSchemasScript, VALIDATE_RUNTIME } from "@wrnexus/validation";
import { signupSchema } from "./app/schemas/signup.ts";

const head = `<script>${renderSchemasScript({ signup: signupSchema.describe() })}</script>
<script>${VALIDATE_RUNTIME}</script>`;
// render a <form data-schema="signup"> 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<typeof schema> when a named output type is useful:

const accountSchema = v.object({
  email: v.string().email(),
  attempts: v.number().integer(),
});

type AccountInput = InferSchema<typeof accountSchema>;
const account = parseOrThrow(accountSchema, input);
// account.email: string
// account.attempts: number

Enable validationPlugin() for:

  • <ValidationSummary />
  • <FieldError />

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:

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.