Validationatthespeedoflight.

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/vld
  • 0 dependencies
  • 259/259 zod exports
  • 32 locales
  • 2633 tests
  • 100% coverage
  • MIT
payload.json
—
safeParse()
VALID
result.success === true

Parsed and type-narrowed without a single cast. The compiler already knows this shape.

type User ={
  email: string
  password: string
  age: number
  handle: string
  role: "admin" | "user" | "guest"
  website?: string
}
Runs the real @oxog/vld bundle in your browser — no mock.
throughput

Numbers the CI suite refuses to let regress.

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.

vld zod 4.5
Simple stringstring().min(1)
3.0x
620M
205M
Number · positive intnumber().int().positive()
9.1x
253M
27.8M
Discriminated union3-member union
4.1x
35.0M
8.5M
Object parse{ a: string, b: number }
1.7x
49.0M
28.8M
Nullishstring().nullish()
30.6x
214M
7.0M
Optional parsenumber().optional()
3.8x
213M
56.0M
Throughput
11x+
vs Zod, release gated
Memory
4.7x less
1.6–10x across schemas
V2 vs Zod 4.6.4
3.03x
10/10 honest wins

what you get

Everything a validation layer needs, nothing it doesn't.

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.

30.7xfastest case

Zero dependencies

Pure TypeScript. No transitive tree, no audit noise, nothing to keep patched.

0runtime deps

Drop-in Zod parity

Swap the import. VLD mirrors Zod's export surface — 259 of 259 exports on 4.6.4 — down to subpath and error shape.

28/28parity tests

32 locales out of the box

Error messages ship in 32 languages, eagerly or lazily loaded, RTL-aware. One call switches the whole surface.

32languages

Full static inference

v.infer<typeof schema> extracts the exact type, no separate type declaration to drift out of sync.

Tree-shakeable mini

@oxog/vld/mini is standalone functions with zero wrapper overhead — the smallest possible parse payload.

Result pattern

Ok, Err, match, tryCatch — functional error handling without exceptions or try/catch noise.

Bidirectional codecs

stringToNumber, jsonCodec, base64ToBytes, hexToBytes — declare the transform once, get decode and encode for free.

100% coverage, 2633 tests

Every validator is tested against real application suites, not just happy-path unit fixtures.

100%coverage

Plugin system

define your own validators once, register them globally, use them exactly like the built-ins.

vld CLI

Schema tooling ships in the package. No extra global install, no config file to keep in sync.

AOT compilation

validate() lazily compiles to source on first call — 2.8x to 33x faster than zod.validate(), and CSP-friendly with withParser.

33xvalidate()
internals

The V2 pattern, and why it wins.

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.

01
Single __def

One field holds the whole validator state. Chain methods return a new def instead of shadowing fields onto the instance.

02
Checks as objects

Constraints are class instances with a check(value) method. Nothing is allocated per parse call, so the hot path stays allocation-free.

03
Precomputed isSimple

Whether a validator can take a fast path is computed once at construction, then read as a boolean on every parse.

3.03x
vs Zod 4.6.4
10/10
head-to-head wins
1.6–10x
less memory

Non-breaking: v.* still returns V1 by default. Opt in with import { vV2 as v } or flip v.setV2Mode(true).

v2-pattern.ts
// 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;
  }
}
mini.ts
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.

migration

Migrating from Zod is one changed line.

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.

  1. 1
    Install

    Same package manager you already use. One dependency, zero transitive tree.

  2. 2
    Swap the import

    z becomes v. Every chain method keeps its name, arity and error shape.

  3. 3
    Run parity

    259 of 259 Zod 4.6 exports verified per release by the CI parity script.

Read the migration guide
migration.diff
// 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() });
src/internal/pattern.ts
// 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;
  }
}
@oxog/vld
main entry
@oxog/vld/v4/core
zod core shape
@oxog/vld/mini
tree-shakeable
@oxog/vld/v4-mini
mini + zod shape
getting started

Five minutes from zero to validated.

Pick a tab. Every snippet here runs against the published package — nothing is simplified for the sake of the docs.

user.ts
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 issues

The full happy path: schema, inferred type, and a parse that never throws.

api surface

Chainable, composable, unsurprising.

v.string()Chainable string validator
v.number()Chainable number validator
v.int() / v.int32()Integer constraints
v.boolean()Boolean validation
v.bigint()BigInt validation
v.date()Date validation
v.symbol()Symbol validation
v.literal(x)Exact value match
v.enum('a','b')Closed value set
v.any() / unknown()Escape hatches
internationalization

Your users read errors in their own language.

Pick a language. This is the actual @oxog/vld bundle validating the same invalid payload — switch locale, re-parse.

32 locales registered
setLocale('en')safeParse
const r = schema.safeParse({ email:'not-an-email', age: -3 });
emailInvalid field "email": Invalid email address
ageInvalid field "age": Number must be positive
lazy: @oxog/vld/locales/lazy
One call, whole surface

setLocale() rewrites every validator's messages at once — no per-schema wiring.

Lazy when it matters

setLocaleAsync() dynamic-imports a single language, so you ship only what you use.

RTL aware

Arabic ships with correct direction handling, not just translated strings.