by M2Toolkit · v0.1.0

Modern validation.
Precise errors.

TypeScript-first schema validation with JSON Schema support. Type-safe by design.

npm install valiq-js

Why valiq-js

One schema.
Types, validation and errors.

Learn more

Describe your data once and get a runtime validator, an inferred TypeScript type, structured errors that are safe to send to clients or write to logs, and JSON Schema in both directions.

Build schemas in TypeScript, or compile existing JSON Schema documents. Both produce the same kind of schema with the same API, and they nest inside each other. Framework-agnostic: no dependencies, no Node.js built-ins, and the same code runs in Node.js and the browser.

ts
import { v, type Infer } from 'valiq-js';

const User = v.object({
  id: v.string().uuid(),
  email: v.string().email(),
  age: v.number().int().min(18),
  roles: v.array(v.enum(['admin', 'editor', 'viewer'])).default(['viewer']),
});

export type User = Infer<typeof User>;
// { id: string; email: string; age: number; roles: ("admin" | "editor" | "viewer")[] }

export function createUser(body: unknown) {
  const result = User.safeParse(body);
  if (!result.success) return { status: 400, issues: result.error.issues };
  return { status: 201, user: result.data }; // typed as User, roles defaults to ['viewer']
}

Precise. Typed. Dependency-free.

Errors you can act on

Every issue has a stable code, the exact path, a readable message and the constraint that failed. Messages never contain the submitted value, so issues are safe to log or return in an API response.

ts
import { v } from 'valiq-js';

const Order = v.object({
  items: v.array(v.object({ sku: v.string().min(3), qty: v.number().int().min(1) })).min(1),
});

const result = Order.safeParse({ items: [{ sku: 'AB', qty: 0 }] });
if (!result.success) {
  console.log(result.error.issues);
  // [{ code: 'too_small', path: ['items', 0, 'sku'], message: 'Expected at least 3 characters',
  //    origin: 'string', minimum: 3, inclusive: true }, ...]
  console.log(result.error.format());
  // ✖ Expected at least 3 characters
  //   → at items[0].sku
}

Strong type inference

Optional keys, defaults (input and output types differ), unions, tuples, records, brands and transforms are all inferred with Infer and Input.

ts
import { v, type Infer, type Input } from 'valiq-js';

export const Settings = v.object({
  theme: v.enum(['light', 'dark']).default('light'),
  fontSize: v.number().int().min(8).optional(),
  tags: v.array(v.string()).max(5),
});

export type Settings = Infer<typeof Settings>;
// { theme: 'light' | 'dark'; tags: string[]; fontSize?: number | undefined }

export type SettingsInput = Input<typeof Settings>;
// { tags: string[]; theme?: 'light' | 'dark' | undefined; fontSize?: number | undefined }

JSON Schema, both ways

Compile JSON Schema 2020-12, 2019-09 and draft-07 with fromJSONSchema, export with toJSONSchema. A typo like maxLenght throws at startup instead of silently accepting everything.

ts
import { fromJSONSchema } from 'valiq-js/json-schema';

export const Product = fromJSONSchema({
  type: 'object',
  properties: {
    sku: { type: 'string', pattern: '^[A-Z]{3}-\\d{4}$' },
    name: { type: 'string', maxLenght: 80 },
  },
  required: ['sku'],
});
// JSONSchemaError: Unknown keyword "maxLenght" (did you mean "maxLength"?)

Works with your stack

Implements Standard Schema, so tRPC, TanStack Form, React Hook Form's Standard Schema resolver and Hono's standard validator accept valiq-js schemas directly.

ts
import { v } from 'valiq-js';

const Login = v.object({ email: v.string().email(), password: v.string().min(8) });

// Any library that accepts a Standard Schema accepts a valiq-js schema:
export const result = await Login['~standard'].validate({ email: 'dev@example.com', password: 'hunter22!' });
  • tRPC
  • TanStack Form
  • React Hook Form
  • Hono

Safe by default

Resistant to prototype pollution and ReDoS in built-in formats, with resource limits for recursion depth, issue count and oversized arrays. No eval or new Function, so it works under a strict Content Security Policy.

  • Own properties only inherited values are treated as missing
  • Input is never mutated defaults are cloned for each use
  • Linear-time formats email, url, uuid, ip, dates
  • Resource limits maxDepth 512 · maxIssues 100
  • CSP-safe no code generation

Validation, not hidden transformation

Valid input is returned as the same object. Nothing is coerced, copied or stripped unless you ask for it with v.coerce.*, .default(), .strip() or .transform().

Playground

Try it live. Edit the input, watch the issues.

Every result below is computed in your browser by the same valiq-js source this page documents. Nothing is mocked.

Schemasignup.ts
ts
import { v } from 'valiq-js';

export const Signup = v
  .object({
    name: v.string().trim().min(2),
    email: v.string().trim().toLowerCase().email(),
    password: v.string().min(12),
    confirm: v.string(),
    age: v.number().int().min(18).optional(),
    newsletter: v.boolean().default(false),
  })
  .refine((form) => form.password === form.confirm, { message: 'Passwords do not match', path: ['confirm'] });
Input
✗ 5 issues
  • too_smallname

    Expected at least 2 characters

    origin: string · minimum: 2 · inclusive: true

  • invalid_formatemail

    Invalid email address

    format: email

  • too_smallpassword

    Expected at least 12 characters

    origin: string · minimum: 12 · inclusive: true

  • invalid_typeage

    Expected integer, received number

    expected: integer · received: number

  • too_smallage

    Expected a number greater than or equal to 18

    origin: number · minimum: 18 · inclusive: true

Fix every field and the cross-field refine runs: make confirm differ from password to see it. Read about issues →

0Zero dependencies
9.5 kBmin+gzip for an object schema
3,009official JSON Schema suite tests passed
ESM + CJSNode.js 18+ and modern browsers

Documentation

Everything, from your first schema to JSON Schema conformance.

Start validating with valiq-js

Describe your data once. Get types, validation and errors you can act on.