Release-gated speed
CI refuses to merge a commit that slows the parse hot path. 11x+ throughput and 4.7x less memory than Zod, measured on every push — not promised once in a README.
A zero-dependency TypeScript validation library with drop-in Zod parity, 32 built-in locales, and a parse hot path so short your CI has to guard it from regressing.
$ npm install @oxog/vldParsed and type-narrowed without a single cast. The compiler already knows this shape.
Every commit runs the benchmark gate against Zod. If a change makes the hot path slower or heavier, the build fails. These bars are repo data, not marketing copy.
what you get
CI refuses to merge a commit that slows the parse hot path. 11x+ throughput and 4.7x less memory than Zod, measured on every push — not promised once in a README.
Pure TypeScript. No transitive tree, no audit noise, nothing to keep patched.
Swap the import. VLD mirrors Zod's export surface — 259 of 259 exports on 4.6.4 — down to subpath and error shape.
Error messages ship in 32 languages, eagerly or lazily loaded, RTL-aware. One call switches the whole surface.
v.infer<typeof schema> extracts the exact type, no separate type declaration to drift out of sync.
@oxog/vld/mini is standalone functions with zero wrapper overhead — the smallest possible parse payload.
Ok, Err, match, tryCatch — functional error handling without exceptions or try/catch noise.
stringToNumber, jsonCodec, base64ToBytes, hexToBytes — declare the transform once, get decode and encode for free.
Every validator is tested against real application suites, not just happy-path unit fixtures.
define your own validators once, register them globally, use them exactly like the built-ins.
Schema tooling ships in the package. No extra global install, no config file to keep in sync.
validate() lazily compiles to source on first call — 2.8x to 33x faster than zod.validate(), and CSP-friendly with withParser.
V3 ships the V2 shape as opt-in factories. It follows Zod 4.5's memoization idea without the per-instance bound-function trick, so you get the same semantics and a smaller, faster footprint.
One field holds the whole validator state. Chain methods return a new def instead of shadowing fields onto the instance.
Constraints are class instances with a check(value) method. Nothing is allocated per parse call, so the hot path stays allocation-free.
Whether a validator can take a fast path is computed once at construction, then read as a boolean on every parse.
Non-breaking: v.* still returns V1 by default. Opt in with import { vV2 as v } or flip v.setV2Mode(true).
// Single __def per instance. Chain methods derive a new def.
// No per-instance field shadowing, no bound functions.
class VldStringV2 {
__def: { checks: Check[]; type: 'string' };
min(n: number) {
return this.withDef({
...this.__def,
checks: [...this.__def.checks, new VldCheckMin(n)],
});
}
check(value: unknown): Issue | null {
for (const c of this.__def.checks) {
const issue = c.check(value);
if (issue) return issue;
}
return null;
}
}import { string, number, object, optional } from '@oxog/vld/mini';
// Standalone functions — zero wrapper overhead,
// fully tree-shakeable.
const userSchema = object({
name: string().min(2),
age: optional(number().positive()),
});V2 validators compose as children of V1 composites and vice versa — migrate one hot path at a time instead of all at once.
VLD deliberately mirrors Zod's API surface instead of inventing a new one. Subpaths, error shapes, and issue payloads line up — so the diff stays reviewable and your tests barely move.
Same package manager you already use. One dependency, zero transitive tree.
z becomes v. Every chain method keeps its name, arity and error shape.
259 of 259 Zod 4.6 exports verified per release by the CI parity script.
// before
import { z } from 'zod';
const schema = z.object({ email: z.string().email() });
// after — one line changed
import { v } from '@oxog/vld';
const schema = v.object({ email: v.string().email() });// Single __def per instance. Chain methods derive a new def.
// No per-instance field shadowing, no bound functions.
class VldStringV2 {
__def: { checks: Check[]; type: 'string' };
min(n: number) {
return this.withDef({
...this.__def,
checks: [...this.__def.checks, new VldCheckMin(n)],
});
}
check(value: unknown): Issue | null {
for (const c of this.__def.checks) {
const issue = c.check(value);
if (issue) return issue;
}
return null;
}
}Pick a tab. Every snippet here runs against the published package — nothing is simplified for the sake of the docs.
import { v } from '@oxog/vld';
const userSchema = v.object({
name: v.string().min(2).max(100),
email: v.string().email(),
age: v.number().int().positive().optional(),
role: v.enum('admin', 'user', 'guest').default('user'),
});
type User = v.infer<typeof userSchema>;
const result = userSchema.safeParse(payload);
// result.data -> typed as User
// result.error -> VldError with structured issuesThe full happy path: schema, inferred type, and a parse that never throws.
v.string()Chainable string validator01v.number()Chainable number validator02v.int() / v.int32()Integer constraints03v.boolean()Boolean validation04v.bigint()BigInt validation05v.date()Date validation06v.symbol()Symbol validation07v.literal(x)Exact value match08v.enum('a','b')Closed value set09v.any() / unknown()Escape hatches10Pick a language. This is the actual @oxog/vld bundle validating the same invalid payload — switch locale, re-parse.
const r = schema.safeParse({ email:'not-an-email', age: -3 });
setLocale() rewrites every validator's messages at once — no per-schema wiring.
setLocaleAsync() dynamic-imports a single language, so you ship only what you use.
Arabic ships with correct direction handling, not just translated strings.