@pyreon/validate — API Reference
Generated from
validate'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see validate.
Pyreon-owned validator library implementing Standard Schema (https://standardschema.dev) natively. Hybrid API: chainable methods (s.string().email().min(3)) AND function composition (pipe(string(), email(), min(3))) — same schemas, different ergonomic surface. The chainable path doesn't pay class-overhead per parse: each schema's ops compile to a single closure on first call. Includes built-in DX helpers (withField / parseReactive / formatErrors) that ALSO work on top of any other Standard Schema validator (Zod 3.24+, Valibot 1.0+, ArkType 2.0+) — backward-compatible with existing schema code. v1 surface: string/number/boolean/bigint/date/literal/enum/symbol/null/undefined/void/any/unknown primitives + object/array/tuple/record/union/discriminatedUnion/intersection/map/set/lazy composition + optional/nullable/nullish/default/transform/refine/brand/describe modifiers + coercion + 20+ built-in checks. Object algebra: .pick / .omit / .partial / .required / .extend / .merge / .keyof + unknown-key policy (.strip / .strict / .passthrough / .catchall). Performance (bench:validation vs Zod 4 / Valibot 1 / ArkType 2 — per-cell process-isolated with 3 processes pooled, cross-library correctness gate, seeded bootstrap CI95 with tie detection; author-written-and-judged, disclosed in the bench header): FASTEST on the error/invalid path across every shape (33–44× vs Zod, 20–53× vs ArkType, 1.4–3.6× vs Valibot — early-exit vs rich error allocation, with verified error-information parity vs Zod); valid-parse WINS arrays (1.9–2.5×), scalar-email (~1.3×), number-range (~1.9×) and deep-nested (~1.5×) outright vs ArkType and is statistically TIED 🤝 on flat-object + discriminated-union (the former ~1.2× flat-object deficit closed by the 2026-07 pure-JIT reused-ctx seam — see below; ArkType still returns the INPUT by reference while Pyreon returns an immutable stripped clone, a semantic choice), ahead of Zod + Valibot on every shape. The JIT (core/jit.ts) SHIPS and auto-applies on first .parse(): it recursively flattens pure object/array/primitive/discriminated-union shapes to one monomorphic new Function with static-path elision (ctx.path untouched on the valid path; full issue paths reconstructed only at failure sites) and async-aware fallback deferral (async .refine/.transform/.serverCheck subtrees park on a pending list the root return awaits), falling back to the interpreter per-subtree for anything it can't inline (plain union/record/tuple/coerce/…). A fully-inline tree (no fallback ⇒ provably no user code, no Promise return, no pending) is branded _jitPure, and the public seams (parse / ~standard.validate / is) then REUSE a per-schema parse context — zero per-parse ctx/issues-array allocation on the monomorphic hot loop (semantics byte-identical to the general seam; locked by a 10,000-case pure-seam differential fuzz). A discriminated union compiles to a raw-value discriminant switch (=== is exactly the interpreter's Map.get SameValueZero for every tag except NaN — a NaN-tagged union keeps the captured-Map dispatch) with member object bodies inlined, and all-inline-primitive object bodies construct their strip-clone as ONE object literal (final shape in a single allocation). Both paths are byte-identical, locked by FOUR JIT↔interpreter differential fuzz suites (sync inline kinds, the partial-inline seam, async trees, discriminated-union roots). The hot email 'standard' tier runs a table-driven charcode scanner (~1.6× the Zod-parity regex, exhaustive+fuzz equivalence-locked).
Features
withField(schema, meta) — attach label/hint/placeholder/i18n keys to ANY Standard Schema validator
getMeta(schema) / resolveMetaField(schema, field, t) — read metadata, optionally i18n-resolved
parseReactive(schema, signal) — Computed<ParseResult> that re-derives on signal changes
parseReactiveAsync — async variant for schemas with async refinements
watchValid(schema, signal, callback) — fires on validity transitions, not every error change
formatError / formatErrors / formatErrorsByPath — i18n-key-aware error rendering
Works with any Standard Schema validator (Zod 3.24+, Valibot 1.0+, ArkType 2.0+, …)
Tree-shakeable — DX helpers alone are ~0.5KB gz (measured); the v1 validator runtime (~3.5KB gz) is pulled in only when
s/ primitives are importedClient/server split — ONE shared schema, thin client + heavy server.
s.string().email()validates strictly server-side (rfc5322 + disposable blocklist, full E.164 phone) the instant@pyreon/validate/serveris imported — the heavy code tree-shakes out of the client bundle..serverCheck(key)is the async/privileged tier (unique-email, breach-check, MX): a no-op on the client (recorded onResult.pending), the registered validator (viaregisterServerCheck) on the server.parseAsync(input, { context })threads a DB handle / request to the server checks.String format checks: email / url / uuid / ip / phone / creditCard + cuid2 / ulid / nanoid / emoji / base64 / jwt + ISO date/dateTime/time — every format routed through the client/server registry seam, so a server can upgrade any of them in place via
installFormatValidator..catch(fallback)— resilient parse: on failure, discard issues and substitute a static or input-derived fallback (terminal regardless of chain position; sync + async)..readonly()— freeze the parsed output +Readonly<T>at the type level.
Complete example
A full, end-to-end usage of the package:
import { z } from 'zod'
import { signal } from '@pyreon/reactivity'
import { useI18n } from '@pyreon/i18n'
import {
withField,
getMeta,
parseReactive,
watchValid,
formatErrors,
} from '@pyreon/validate'
// 1. Attach Pyreon field metadata to any Standard Schema.
const emailSchema = withField(z.string().email(), {
label: 'Email address',
placeholder: 'you@example.com',
hint: 'We never share your email',
i18nLabel: 'auth.email.label',
i18nHint: 'auth.email.hint',
autoComplete: 'email',
})
// emailSchema is STILL a Zod schema — every Zod method works:
emailSchema.parse('foo@bar.com') // → 'foo@bar.com'
emailSchema['~standard'].validate('bad') // → { issues: [...] }
// Read the metadata back:
getMeta(emailSchema)
// → { label: 'Email address', placeholder: ..., i18nLabel: ..., ... }
// 2. Reactively parse a signal-backed input.
const $email = signal('')
const $result = parseReactive(emailSchema, $email)
effect(() => {
const r = $result()
if (r.issues) console.warn('invalid:', r.issues)
else console.log('parsed:', r.value)
})
$email.set('foo@bar.com') // → $result fires, valid
// 3. Watch for validity flips (no fire on error-message change).
const stop = watchValid(emailSchema, $email, (valid) => {
submitButton.disabled = !valid
})
// 4. i18n bridge — resolve issue keys to translated strings.
const { t } = useI18n()
const messages = formatErrors($result().issues ?? [], t)
// → translated strings via t(issue.key, issue.params), with fallback to issue.fallback or issue.message
// 5. Works with ANY Standard Schema validator — drop-in swap to Valibot:
import * as v from 'valibot'
const sameSchema = withField(v.pipe(v.string(), v.email()), { label: 'Email' })
const $sameResult = parseReactive(sameSchema, $email)Exports
| Symbol | Kind | Summary |
|---|---|---|
withField | function | Attach Pyreon field metadata (label, hint, placeholder, i18n keys, autoFocus, autoComplete, defaultValue) to any Standar |
getMeta | function | Read the Pyreon field metadata attached via withField(). |
resolveMetaField | function | Read a metadata field through optional i18n. |
parseReactive | function | Reactively parse source through schema. |
parseReactiveAsync | function | Async variant of parseReactive. |
watchValid | function | Subscribe to validity transitions. |
formatError | function | Resolve a single issue to a human-readable string. |
formatErrors | function | Resolve an array of issues to strings via the same per-issue logic as formatError. |
formatErrorsByPath | function | Build a per-field error map keyed by the issue's path joined with .. |
toJsonSchema | function | Emit a JSON Schema (draft 2020-12) document from an s schema — for OpenAPI specs, AI structured-output constraints, ed |
serverCheck | function | Declare a server-only validation step on a shared schema — the async/privileged tier of the client/server split (unique- |
registerServerCheck | function | Register the heavy/privileged half of a .serverCheck(key) — the implementation that must NEVER reach the client bundle |
catch | function | On parse FAILURE, discard the issues this schema produced and return a fallback instead of erroring — resilient parsing |
readonly | function | Freeze the parsed output and mark it Readonly<T> at the type level (Zod's .readonly). |
array | function | Wrap this schema in an array — s.string().array() ≡ s.array(s.string()) (Zod's .array). |
or | function | Union this schema with another — a.or(b) ≡ s.union(a, b) (Zod's .or). |
and | function | Intersect this schema with another — a.and(b) ≡ s.intersection(a, b) (Zod's .and). |
pipe | function | Validate with this schema, then feed the (validated, transformed) output into target (Zod's .pipe). |
superRefine | function | Like .refine, but the callback may add ANY number of issues (or none) via ctx.addIssue({ message, path? }) — for cro |
preprocess | function | Transform the raw input BEFORE schema validates it (Zod's z.preprocess) — for trim/coerce/normalize that must happen |
nonoptional | function | Reject undefined (Zod 4's .nonoptional) — re-requires a present value, e.g. |
stringbool | function | Coerce a boolean-ish STRING to a real boolean (Zod 4's z.stringbool). |
never | function | Accepts NO value (Zod's z.never) — every input is a validation error, including undefined. |
custom | function | Escape-hatch validated by a user predicate (Zod's z.custom<T>). |
instanceof | function | Asserts input instanceof Ctor (Zod's z.instanceof). |
nativeEnum | function | Validate a VALUE of a TS native enum (or a const value-object) — Zod's z.nativeEnum. |
API
withField function
<S extends StandardSchemaV1>(schema: S, meta: FieldMeta) => SAttach Pyreon field metadata (label, hint, placeholder, i18n keys, autoFocus, autoComplete, defaultValue) to any Standard Schema. The returned schema is the SAME REFERENCE as the input — Pyreon mutates a Symbol-keyed non-enumerable slot in place, which is invisible to JSON serialization, for…in, Object.keys, and library-internal comparators. Mutation (instead of cloning) is required because ArkType's Type instances are callable functions whose ~standard.validate does this(input) — a shallow clone would not be callable and would break that contract. Re-wrapping merges new metadata onto existing (later keys win).
Example
const emailSchema = withField(z.string().email(), {
label: 'Email address',
placeholder: 'you@example.com',
i18nLabel: 'auth.email.label',
autoComplete: 'email',
})Common mistakes
Expecting withField to return a NEW reference — it doesn't. The metadata mutation is in place. If you need an isolated copy, construct two separate schemas instead.
Adding
i18nLabelwithout a correspondinglabel— without a translation provider (or when t echoes the key), there's no fallback. Always set both.Storing schemas with metadata in JSON.stringify-d state and round-tripping — the metadata is Symbol-keyed and won't survive serialization. Re-attach on load.
See also: getMeta · resolveMetaField · StandardSchemaV1
getMeta function
<S extends StandardSchemaV1>(schema: S) => FieldMeta | undefinedRead the Pyreon field metadata attached via withField(). Returns undefined for schemas that haven't been wrapped — consumers should be defensive (getMeta(schema)?.label ?? fallback). Accepts both objects AND functions (ArkType's Type instances are callable).
Example
const meta = getMeta(emailSchema)
const label = meta?.label ?? humanize(fieldName)Common mistakes
Metadata is a Symbol slot on ONE schema object; modifiers create NEW instances that don't copy it —
withField(s.string(), {...}).optional()returns an OptionalSchema without the slot, sogetMetayieldsundefined. Attach metadata to the OUTERMOST schema (after.optional()/.transform())getMetareturnsundefinedfor any un-withFielded schema — always be defensive:getMeta(x)?.label ?? fallbackresolveMetaFieldonly handleslabel/hint/placeholder— other i18n keys aren't resolvable through iti18n resolution needs a
tthat does NOT echo the key — withoutt(or whentreturns the key unchanged) it falls back to the literal metadata value
See also: withField · resolveMetaField
resolveMetaField function
<S extends StandardSchemaV1>(
schema: S,
field: 'label' | 'hint' | 'placeholder',
t?: TFn,
) => string | undefinedRead a metadata field through optional i18n. If the metadata has an i18n<Field> key AND a t function is provided AND t resolves it (returns a non-key string), the resolved string wins. Otherwise falls back to the literal. Recommended over getMeta(schema)?.label directly when you have a t from useI18n().
Example
const label = resolveMetaField(emailSchema, 'label', t)
// → t('auth.email.label') if set + resolved, else meta.label, else undefinedSee also: getMeta · formatErrors
parseReactive function
<S extends StandardSchemaV1>(
schema: S,
source: Signal<unknown> | (() => unknown),
) => Computed<ParseResult>Reactively parse source through schema. Returns a Computed<ParseResult> that re-validates on every source change. Synchronous only — for schemas with async refinements (Zod .refine(async), Valibot async pipe), use parseReactiveAsync (this sync variant surfaces an actionable issue if the schema returns a Promise).
Example
const $email = signal('')
const $result = parseReactive(emailSchema, $email)
effect(() => {
const r = $result()
if (r.issues) showError(r.issues)
else commitValue(r.value)
})
$email.set('foo@bar.com') // $result re-derivesCommon mistakes
Using parseReactive on an async schema — it surfaces a clear "use parseReactiveAsync" issue rather than silently producing a Promise as the validation result.
Calling parseReactive on every render of a component — it allocates a Computed; cache it at component setup time (call once per signal-source pair).
See also: parseReactiveAsync · watchValid · formatErrors
parseReactiveAsync function
<S extends StandardSchemaV1>(
schema: S,
source: Signal<unknown> | (() => unknown),
) => Computed<Promise<ParseResult>>Async variant of parseReactive. The outer Computed re-evaluates synchronously on source change; the inner Promise resolves once the validator finishes. Stale results are superseded automatically — each re-run bumps an internal version, and a validation that finishes after a newer one started resolves to the NEWEST run's result, so an awaited stale frame can never deliver a stale verdict.
Example
const schema = z.string().refine(async (s) => await checkUnique(s))
const $result = parseReactiveAsync(schema, $username)
watch($result, async (current) => {
const r = await current
showFeedback(r)
})Common mistakes
Adding your own debounce/version counter to guard against stale results — unnecessary: each re-run bumps an internal version and a stale frame's promise FORWARDS to the newest run's result, so an awaited stale frame resolves to the LATEST run's verdict. What it does NOT do is abort the in-flight validator (no AbortSignal — a slow async refine still runs to completion; only its result is superseded)
Read
sourcesynchronously at the top of your accessors — the async computed tracks only thesourceread that runs before the firstawait; a signal read placed after an await won't re-triggerDon't call it per render — it allocates a
Computed; create it once per (schema, source) pair at setupThe
~standardpath it uses drops the Pyreonpendingserver-check info — callschema.parseAsync()directly if you need to surface deferred server checks
See also: parseReactive
watchValid function
<S extends StandardSchemaV1>(
schema: S,
source: Signal<unknown> | (() => unknown),
callback: (valid: boolean) => void,
) => () => voidSubscribe to validity transitions. The callback fires only when validity flips (true→false or false→true), NOT on every error-message change — ideal for form-state hooks that care about "is this OK?" without re-rendering on every typo. Returns an unsubscribe function. Internally a watch() over parseReactive.
Example
const stop = watchValid(emailSchema, $email, (valid) => {
submitButton.disabled = !valid
})
onUnmount(stop)See also: parseReactive
formatError function
(issue: StandardSchemaIssue | PyreonIssue, t?: TFn) => stringResolve a single issue to a human-readable string. Resolution order: (1) issue.key + t provided AND t returns a non-key string → resolved string; (2) issue.fallback if set; (3) issue.message (always present per StdSchema spec). Native StdSchema issues without key/fallback fall through to message immediately — no overhead.
Example
const message = formatError(issue, t)
// → t('validate.string.too-short', { min: 2 }) when key + t resolve
// → issue.fallback ('Must be at least 2 characters') when t echoes the key
// → issue.message (raw lib message) when no key at allSee also: formatErrors · formatErrorsByPath
formatErrors function
(issues: ReadonlyArray<StandardSchemaIssue | PyreonIssue>, t?: TFn) => string[]Resolve an array of issues to strings via the same per-issue logic as formatError. Returns strings in the original order so paths line up with the input array.
Example
const { t } = useI18n()
const messages = formatErrors(result.issues ?? [], t)Common mistakes
Resolution order is strict —
issue.key+ atthat returns a NON-key string resolves; elseissue.fallback; elseissue.message. WITHOUT atargument i18n keys never resolve and it drops straight to fallback/messageA missing translation is SILENT: if
t(key)echoes the key back (no entry) it falls through tofallback/messagerather than showing the raw key — always set afallbackalongside akeyNative Zod/Valibot/ArkType issues carry no
key/params/fallback, so it returns their raw.messageregardless oft— only Pyreon-issue shapes route through i18nparamsare the SECOND arg oft(issue.key, issue.params)— your i18n interpolation must read them from thereformatErrorsByPathkeeps only the FIRST issue per path unless you passjoinWith, and path-less/form-level issues land under the''key
See also: formatError · formatErrorsByPath
formatErrorsByPath function
(issues: ReadonlyArray<StandardSchemaIssue | PyreonIssue>, t?: TFn, options?: { joinWith?: string }) => Record<string, string>Build a per-field error map keyed by the issue's path joined with .. Compatible with @pyreon/form's Errors shape (Partial<Record<fieldName, string>>). Path-less issues land under the empty-string key. First issue wins on collision unless joinWith is set (then messages concatenate).
Example
const errorMap = formatErrorsByPath(result.issues ?? [], t)
// → { email: 'Invalid email', password: 'Too short', ... }See also: formatErrors
toJsonSchema function
(schema: Schema<unknown>, options?: { unrepresentable?: 'throw' | 'any' }) => JsonSchemaEmit a JSON Schema (draft 2020-12) document from an s schema — for OpenAPI specs, AI structured-output constraints, editor autocomplete, cross-language contracts. Ships on the @pyreon/validate/json-schema SUBPATH so the main entry stays lean. The document describes the INPUT shape: .transform() emits its inner schema, .pipe() its source, s.preprocess() its target; .refine()/.superRefine()/.serverCheck() are runtime-only predicates and are structurally omitted. Formats map to standard format keywords (email/uri/uuid/date/date-time/time/duration); regex/startsWith/endsWith/includes map to pattern; .int() upgrades to type: 'integer'; .strict() → additionalProperties: false, .catchall(s) → additionalProperties: <schema>; .optional()/.nullish()/.default() fields are omitted from required.
Example
import { toJsonSchema } from '@pyreon/validate/json-schema'
toJsonSchema(s.object({ name: s.string().min(2), age: s.number().int().optional() }))
// → { $schema: 'https://json-schema.org/draft/2020-12/schema',
// type: 'object',
// properties: { name: { type: 'string', minLength: 2 }, age: { type: 'integer' } },
// required: ['name'] }Common mistakes
Expecting Date/BigInt/Map/Symbol/undefined kinds to emit — JSON Schema cannot express them; the emitter THROWS a [Pyreon]-prefixed error naming the kind. Pass { unrepresentable: 'any' } to emit {} (accept-anything) in their place instead.
Expecting the post-transform OUTPUT shape — the document describes the INPUT the schema accepts (a
.transform()result type is a runtime concern JSON Schema can't express).Feeding a cyclic s.lazy() schema — recursive $ref/$defs graph emission is not supported in v1; the emitter throws with guidance. Flatten the recursion or write the recursive document by hand.
Importing from the main entry —
toJsonSchemais deliberately on the@pyreon/validate/json-schemasubpath so validators-only bundles never carry the emitter.
See also: object · union
serverCheck function
(key: string, opts?: { message?: string; code?: string; key?: string; params?: Record<string, unknown>; fallback?: string }) => thisDeclare a server-only validation step on a shared schema — the async/privileged tier of the client/server split (unique-email, breach-check, DNS-MX, cross-field DB lookups). On the CLIENT (no validator installed) it's a no-op: the value passes and the deferred check is recorded on Result.pending (so the UX can show a "checking…" affordance). On the SERVER, the validator registered via registerServerCheck(key, fn) runs — sync or async. Async checks promote the parse to parseAsync, which threads an opaque context (DB handle, request) to the validator. Issue path is snapshotted at the check site, so a field/array-element check reports the correct path even though it resolves after the path unwinds.
Example
// shared schema (client + server)
const signup = s.object({
email: s.string().email().serverCheck('email-unique', { message: 'Email already taken' }),
})
// CLIENT: cheap checks run; serverCheck is deferred
const r = signup.parse(formData)
if (r.ok && r.pending?.length) showChecking() // 'email-unique' pending
// SERVER (registerServerCheck installed elsewhere):
const verdict = await signup.parseAsync(formData, { context: { db } })Common mistakes
A client
r.ok === trueis NOT verification — with no validator installedserverCheckis a NO-OP that passes the value and only records apendingentry; the SERVERparseAsyncis the authoritative re-validationForgetting to
registerServerCheck(key, fn)on the server (e.g. not importing the server-only module) silently PASSES — an unregistered key defers topending, it never failsA registered ASYNC check promotes the parse to async, so plain
.parse()bails with a[Pyreon] schema is async — use parseAsyncissue — callschema.parseAsync(input, { context })The DB handle / request only reaches your validator via
parseAsync(input, { context })— plain.parse()leavesctx.contextundefinedImport
registerServerCheckonly from@pyreon/validate/server(a side-effecting server-only entry) — importing it into client code drags the heavy validators into the client bundleAny schema with a
serverCheckanywhere in its tree skips the JIT (it can't await) and silently runs the slower interpreter path
See also: registerServerCheck · parseAsync
registerServerCheck function
(key: string, fn: (value: unknown, context?: unknown) => boolean | Promise<boolean>) => voidRegister the heavy/privileged half of a .serverCheck(key) — the implementation that must NEVER reach the client bundle (DB lookups, breach-checks, MX, cross-field). Imported from @pyreon/validate/server and called from a server-only module; the matching s.…serverCheck(key) in the shared schema then validates here. Returning false fails the check with the schema's message. The second arg is the context passed to parseAsync(input, { context }).
Example
// server-only module
import { registerServerCheck } from '@pyreon/validate/server'
registerServerCheck('email-unique', async (value, ctx) => {
const db = (ctx as { db: Db }).db
return !(await db.user.existsByEmail(value as string))
})See also: serverCheck
catch function
(value: T | ((input: unknown) => T)) => thisOn parse FAILURE, discard the issues this schema produced and return a fallback instead of erroring — resilient parsing (Zod's .catch). The fallback is a static value or a function of the raw input. Terminal regardless of chain position: s.string().min(3).catch('x') and s.string().catch('x').min(3) behave identically. Works on both parse and parseAsync (an async transform/refine failure is caught after the Promise settles). Scoped per-schema: a caught FIELD failure is substituted while sibling failures still fail the object.
Example
s.number().catch(0).parse('nope') // → { ok: true, value: 0 }
s.string().min(3).catch('x').parse('ab') // → { ok: true, value: 'x' }
s.string().catch((input) => String(input)) // fallback derived from the raw inputCommon mistakes
.catch()swallows EVERY issue this schema produced (type failures AND check/refine/transform failures alike) — a genuinely broken input is masked asok: true; scope it narrowly, not around a whole objectA FUNCTION passed to
.catch()is ALWAYS an input→fallback mapper (called with the raw original input), never a literal fallback — you cannot use.catch()to return a function valueThe fallback is returned verbatim and is NOT re-validated — a fallback that doesn't satisfy the schema's type still passes through as
ok: true.catch()is terminal + position-independent with LAST-wins semantics —s.string().catch('a').catch('b')always yields'b'The catch fn receives the RAW original input (captured before the
default/modifier prelude), not the typed or defaulted value
See also: readonly · default
readonly function
() => Schema<ShallowReadonly<T>>Freeze the parsed output and mark it Readonly<T> at the type level (Zod's .readonly). Objects/arrays are Object.freezed (shallow) so accidental downstream mutation throws in strict mode; primitives pass through. Apply last in a chain. Uses a primitive-safe ShallowReadonly<T> (not the built-in Readonly<T>, whose Readonly<unknown> resolves to {} and breaks Schema<T> → Schema<unknown> assignability).
Example
const cfg = s.object({ port: s.number() }).readonly()
const r = cfg.parse({ port: 80 })
// r.value is Readonly<{ port: number }> and Object.isFrozen(r.value) === trueSee also: catch
array function
() => ArraySchema<T>Wrap this schema in an array — s.string().array() ≡ s.array(s.string()) (Zod's .array). Chains and nests (s.number().array().array()). Late-bound via a tree-shake-safe factory registry so the base class never imports the composition modules (no load-order cycle).
Example
s.string().array().parse(['a', 'b']) // → { ok: true, value: ['a', 'b'] }See also: or · and
or function
<U>(other: Schema<U>) => UnionSchema<readonly [Schema<T>, Schema<U>]>Union this schema with another — a.or(b) ≡ s.union(a, b) (Zod's .or). Output type is T | U.
Example
s.string().or(s.number()) // Schema<string | number>Common mistakes
.or()/s.union(...)members MUST be schemas — a non-schema member (or fewer than two) throws a clear[Pyreon]error ONLY whenNODE_ENV !== "production"; a production build strips the guard and a bad member crashes cryptically at parse time (member._runInto is not a function/ reading_runIntoof undefined)A union surfaces NO per-member issues — a total miss yields one opaque
invalid_union"Did not match any allowed type", so you can't tell which member was closest; members are tried in order, first-match wins.or()/.and()/.array()throwCOMPOSITION_UNREGISTEREDif the composition factory was never registered — a bareimport { string }that never referencess/union/intersectionskips registrationAn async member inside a SYNC union parse pushes an
async member … use parseAsyncissue rather than awaiting
See also: and · array
and function
<U>(other: Schema<U>) => IntersectionSchema<T, U>Intersect this schema with another — a.and(b) ≡ s.intersection(a, b) (Zod's .and). Output type is T & U.
Example
s.object({ a: s.string() }).and(s.object({ b: s.number() })) // { a } & { b }See also: or · array
pipe function
<U>(target: Schema<U>) => Schema<U>Validate with this schema, then feed the (validated, transformed) output into target (Zod's .pipe). Ideal for coerce→validate chains. Short-circuits if this schema fails; async-aware. Output type is target's.
Example
s.string().transform(Number).pipe(s.number().positive())See also: preprocess · transform
superRefine function
(fn: (value: T, ctx: SuperRefineCtx) => void) => Schema<T>Like .refine, but the callback may add ANY number of issues (or none) via ctx.addIssue({ message, path? }) — for cross-field validation that reports multiple problems at once. path is appended to the field's current path. Runs only if this schema passed.
Example
s.object({ pw: s.string(), confirm: s.string() }).superRefine((v, ctx) => {
if (v.pw !== v.confirm) ctx.addIssue({ message: 'Mismatch', path: ['confirm'] })
})Common mistakes
Report problems ONLY via
ctx.addIssue(...)— the callback returns void, soreturn falseor returning a message does nothingIt runs ONLY if the base schema produced zero issues — if the underlying type/checks already failed, your cross-field refinement never executes
addIssue({ path })APPENDS to the field's current path — pass a RELATIVEpath: ["confirm"], not an absolute path, or the issue lands at the wrong nested locationDo NOT make the callback async — the signature is sync
=> voidand issues are collected right after it returns, so anything pushed after anawaitis lostsuperRefinereturns a NEW wrapper schema, notthis— capture the return value
See also: refine · pipe
preprocess function
<TOut>(fn: (input: unknown) => unknown, schema: Schema<TOut>) => Schema<TOut>Transform the raw input BEFORE schema validates it (Zod's z.preprocess) — for trim/coerce/normalize that must happen before the type-check. A standalone function (also on the s namespace), not a method.
Example
s.preprocess((v) => String(v).trim(), s.string().min(1))Common mistakes
Argument order is
(fn, target)—fnmaps the RAW input first, thentargetvalidates the mapped value; the output type istarget'sNo type flows from
fnintotarget—fnis typed(unknown) => unknown, so nothing enforces that its return matches whattargetexpects; that alignment is on youKeep
fnTOTAL (never throw) —.parse()does NOT try/catch a sync throw fromfn(onlyparseAsyncdoes), so a throwing preprocess propagates out of.parse()
See also: pipe · transform
nonoptional function
(message?: string) => Schema<Exclude<T, undefined>>Reject undefined (Zod 4's .nonoptional) — re-requires a present value, e.g. after an .optional() in a reused base schema.
Example
s.string().optional().nonoptional() // rejects undefined againCommon mistakes
It rejects
undefinedat RUNTIME (pushes a "Required" issue) — it is not merely a type cast; the runtime guard is what enforces presenceIt rejects ONLY
undefined, NOTnull—s.string().nullish().nonoptional()still acceptsnullThe default message is
"Required"
See also: optional
stringbool function
(opts?: { truthy?: string[]; falsy?: string[]; message?: string }) => StringBoolSchemaCoerce a boolean-ish STRING to a real boolean (Zod 4's z.stringbool). Type-checks a string, then maps configured truthy/falsy tokens (case-insensitive, trimmed; defaults true/1/yes/on/y/enabled ↔ false/0/no/off/n/disabled) to true/false; anything else errors. Stricter than s.coerce.boolean() (which uses JS truthiness on any input).
Example
s.stringbool().parse('yes') // → { ok: true, value: true }
s.stringbool({ truthy: ['si'], falsy: ['no'] })Common mistakes
The default truthy set is exactly
true/1/yes/on/y/enabledand falsyfalse/0/no/off/n/disabled— anything else ('2','maybe', and crucially the EMPTY string) is a validation ERROR, notfalseAn unset env var read as
''FAILSstringbool(empty string is in neither set) — add.default(false)/.optional()when a missing var should mean falseIt accepts ONLY strings — a real boolean or number emits a type issue; use
s.coerce.boolean()(JS truthiness on any input) if you need that, they are NOT interchangeablePassing
truthy/falsyREPLACES the defaults, it does not extend them —stringbool({ truthy: ['yes'] })makes'1'/'true'/'on'invalidMatching is case-insensitive + trimmed, truthy checked before falsy —
' TRUE 'works, and a token in BOTH sets resolves totrue
See also: coerce
never function
() => Schema<never>Accepts NO value (Zod's z.never) — every input is a validation error, including undefined. Used for exhaustiveness and to forbid a key (s.object(...).extend({ legacy: s.never().optional() }) rejects the key only when present; a bare s.never() field is required-and-unsatisfiable).
Example
s.never().parse(1) // → { ok: false }
s.object({ a: s.string() }).extend({ legacy: s.never().optional() })See also: unknown · custom
custom function
<T = unknown>(check?: (value: unknown) => boolean, message?: string) => Schema<T>Escape-hatch validated by a user predicate (Zod's z.custom<T>). With NO predicate it accepts everything as T (a pure type assertion); with one it emits a custom-coded issue when the predicate returns false. The output type is the caller-supplied T — never narrowed, since the predicate is opaque.
Example
s.custom<`${number}px`>((v) => typeof v === 'string' && v.endsWith('px'))
s.custom<MyType>() // accept anything as MyTypeSee also: instanceof · refine
instanceof function
<T>(ctor: new (...args: any[]) => T, message?: string) => Schema<T>Asserts input instanceof Ctor (Zod's z.instanceof). The canonical way to validate runtime class instances — s.instanceof(File), s.instanceof(Date), s.instanceof(URL), user classes. The default message names the class; pass a second arg to override.
Example
s.instanceof(File) // validate an uploaded File
s.instanceof(Date, 'need a Date')Common mistakes
It uses native
input instanceof ctor, so CROSS-REALM instances FAIL — aDate/File/URLfrom an iframe, worker, or vm has a different constructor identity and won't validateIt cannot survive an SSR→client (or any JSON) boundary — a hydrated plain object / date string is not an instance, so
instanceof(Date)rejects deserialized data; validate the serialized shape (an ISO string) and reconstruct insteadThe output is the input unchanged —
instanceofASSERTS, it does not construct or coerce an instance
See also: custom
nativeEnum function
<E extends Record<string, string | number>>(enumObject: E) => Schema<E[keyof E]>Validate a VALUE of a TS native enum (or a const value-object) — Zod's z.nativeEnum. Output type is the enum's value union (E[keyof E]). Correctly filters out the numeric reverse-mappings TS auto-generates (a numeric enum { A } compiles to { A: 0, 0: 'A' }, so 'A' is NOT accepted as input — only 0 is). Use s.enum([...]) instead for a plain literal array.
Example
enum Role { Admin = 'admin', User = 'user' }
s.nativeEnum(Role).parse('admin') // → { ok: true, value: 'admin' }See also: enum · literal
Package-level notes
Standard Schema is parse-only: The protocol deliberately omits a metadata channel — that's the gap
withFieldfills. The protocol also doesn't carry i18n keys —formatErrorsadds that layer.
withField mutates in place: ArkType's Type instances are callable functions whose
~standard.validatedoesthis(input)—thismust be the callable schema itself. An Object.create() clone is not callable and breaks ArkType. Symbol-keyed non-enumerable mutation is invisible to JSON / for…in / Object.keys / library-internal comparators. Safe.
Ships its own validator AND interops with others: v1 ships Pyreon's own
svalidator runtime (chainable + function-comp, Standard Schema-native, opt-in by import). The DX helpers (withField/parseReactive/formatErrors) ALSO work on top of any other Standard Schema validator (Zod / Valibot / ArkType / typia) — backward-compatible. Use whichever; mix freely.
serverCheck is client-no-op, server-async:
.serverCheck(key)only runs where its validator is installed. On the client it ALWAYS passes (recorded onResult.pending) — the SERVER is the authoritative re-validation; never treat a clientok: truewithpendingas fully verified. A registered ASYNC check promotes the parse to a Promise, soparse()returns a parseAsync-directing issue — useparseAsync(input, { context })server-side. serverCheck fields JIT-compile like everything else (the JIT defers async-resolving subtrees onto a pending list its root return awaits) — no perf cliff.
Three shipped performance layers — runtime JIT + two build flags: The runtime JIT (
core/jit.ts) auto-applies on first.parse()and is differential-fuzz-locked against the interpreter (incl. async trees + the partial-inline seam). On top:pyreon({ optimizeValidators: true })rewrites module-levels.chains to the tree-shakeable/miniform (~−41% measured), andpyreon({ compileValidators: true })attaches build-emitted monomorphic.is()verdicts (1.6–3× on hot verdict loops — nanoseconds; only matters in tight.is()loops,.parse()is unchanged). Both build flags recognize statically-analyzable chains only (a dynamically-built or out-of-emit-scope chain — e.g..cuid2(), unions — gracefully stays full-runtime).