Skip to main content

Kanon

κανών - "rule"

Lightweight alternative to Zod. Schema validation with TypeScript inference, coercion, and schema composition.

Kanon is a runtime schema validation library designed for TypeScript projects that need fast, type-safe data validation. It infers TypeScript types directly from your schema definitions, so you define your data shape once and get both validation and type safety. Kanon supports type coercion (coerceString, coerceNumber...), schema transforms (partial, pick, omit...), and offers an asZod wrapper for progressive migration from Zod.

Bundle size:

For a complete app with all schema types (gzip):

  • Kanon : 4.22 kB baseline
  • Zod 4 Mini : 7.50 kB (+78%)
  • Zod 4 Classic : 27.57 kB (+554%)

Quick Example​

Define a schema using composable primitives, then validate incoming data with parse. The result is a discriminated union: either a typed success value or a structured error, so you always know what you're working with:

import { object, string, number, optional, parse } from "@pithos/core/kanon";

const userSchema = object({
name: string().minLength(1),
age: number().min(0).int(),
email: string().email(),
phone: optional(string()),
});

const result = parse(userSchema, data);

if (result.success) {
console.log(result.data); // Typed as { name: string, age: number, ... }
} else {
console.error(result.error);
}

Supported Types​

CategoryTypes
Primitivesstring, number, int, boolean, date, bigint, symbol, null_, undefined_
any, unknown, never, void_
Literals & Enumsliteral, enum_, nativeEnum, ⁠numberEnum⠀⚡️, booleanEnum⠀⚡️, mixedEnum⠀⚡️
Compositesobject, strictObject, looseObject, array, tuple, record, map, set
tupleOf⠀⚡️, tupleOf3⠀⚡️, tupleOf4⠀⚡️, tupleWithRest
OperatorsunionOf, unionOf3⠀⚡️, unionOf4⠀⚡️, intersection, intersection3⠀⚡️
Wrappersoptional, nullable, default_, readonly, lazy
CoercecoerceString, coerceNumber, coerceBoolean, coerceDate, coerceBigInt
Performance

Optimized variants: these functions are more performant than their nested alternatives (e.g., unionOf3(a, b, c) is faster than unionOf(unionOf(a, b), c)). They create a single schema with direct validation instead of nested objects.

Refinements : .min(), .max(), .minLength(), .maxLength(), .email(), .url(), .regex(), .int(), ...


V3 vs JIT​

Kanon v3 offers two validation modes. The standard mode works in any environment, while the JIT compiler generates optimized JavaScript validators at runtime for higher throughput:

ModeSpeedCSP CompatibleUse Case
V3 Standard12.6M ops/s YesDefault, works everywhere
V3 JIT23.6M ops/s Needs unsafe-evalHigh-throughput scenarios
import { parse } from "@pithos/core/kanon";              // Standard
import { compile } from "@pithos/core/kanon/jit/compiler"; // JIT

// JIT: compile once, validate many
const validator = compile(schema);
validator(data); // 2x faster

The JIT compiler analyzes your schema structure and emits a specialized validation function. This avoids the overhead of walking the schema tree on every call. If your environment blocks new Function() via Content Security Policy, Kanon falls back to the standard interpreter automatically.


Key Difference with Zod​

Kanon separates validation and transformation. The native API does not offer chained .transform() pipelines like Zod: you validate first, then transform explicitly in your code.

Coercion (coerceString, coerceNumber...) is the only built-in transformation: it converts the input type before validation (e.g. 42 → "42").

info

The asZod() wrapper supports .transform() and .preprocess() to ease migration from Zod. But in Kanon's native API, prefer handling transformations explicitly after validation.

See also: Kanon vs Zod.


Helpers​

z - Drop-in Replacement for Zod​

Migrate from Zod with a single line change. The z namespace mirrors Zod's API, so your existing schemas and validation calls work without modification. Just swap the import:

// Before (Zod)
import { z } from "zod";

// After (Kanon): only change the import
import { z } from "@pithos/core/kanon/helpers/as-zod.shim";

// Your existing code works unchanged in most cases
Migration

Search & replace from "zod" → from "@pithos/core/kanon/helpers/as-zod.shim" in your codebase. Done.

For the full list of supported Zod features and edge cases, see the Kanon ↔ Zod interoperability matrix.

k - Namespace object​

The k namespace provides the same API as z, using Kanon's own naming conventions. It groups all schema constructors under a single object for convenience:

import { k } from "@pithos/core/kanon/helpers/k";

const schema = k.object({
name: k.string(),
age: k.number().min(0),
});

k.parse(schema, data);
warning

Lighter than asZod, but not tree-shakable: imports all schemas. Prefer direct imports for optimal bundle size.

asZod() - Wrap individual schemas​

Wraps any Kanon schema with Zod-like methods. This is useful when you want tree-shakable imports but still need Zod's fluent API for specific schemas:

import { asZod } from "@pithos/core/kanon/helpers/as-zod";
import { string, number, object } from "@pithos/core/kanon";

const schema = asZod(object({
name: string(),
age: number(),
}));

// Parsing methods
schema.parse(data); // throws on error
schema.safeParse(data); // { success, data/error }
schema.parseAsync(data); // async parse
schema.safeParseAsync(data); // async safeParse

// Refinements & transformations
schema.refine(val => val.age >= 18, "Must be adult");
schema.superRefine((val, ctx) => {
if (val.age < 18) ctx.addIssue({ message: "Too young" });
});
schema.transform(val => ({ ...val, fullName: val.name }));

// Wrappers
schema.optional(); // T | undefined
schema.nullable(); // T | null
schema.default({ name: "Anonymous", age: 0 });
schema.catch({ name: "Fallback", age: 0 });
schema.readonly(); // Readonly<T>
schema.promise(); // Promise<T>

// Operators
schema.array(); // T[]
schema.union(otherSchema); // T | U (alias: .or())
schema.intersection(otherSchema); // T & U (alias: .and())

When NOT to Use​

Kanon is designed for straightforward data validation. For use cases that go beyond checking data shapes, consider these alternatives:

NeedUse Instead
Data utilitiesArkhe
Error handling (Result)Zygos
Complex transformationsCustom logic or Zod

Migrating from Zod​

Step 1: Install Pithos​

Add Pithos to your project using your preferred package manager:

npm install @pithos/core

Step 2: Swap the import​

// Before
import { z } from "zod";

// After
import { z } from "@pithos/core/kanon/helpers/as-zod.shim";

Step 3: Run your tests​

Most schemas work as-is. The z shim covers primitives, objects, arrays, unions, intersections, wrappers, coercion, and refinements.

Step 4: Handle edge cases​

Some Zod features (.pipe(), .brand(), z.instanceof(), specialized string formats like JWT/CUID) are not directly available in Kanon. Workarounds exist for all of them — see the interoperability matrix.

Step 5 (optional): Switch to direct imports​

For maximum bundle optimization, gradually replace the z shim with direct imports:

// z shim (convenient, slightly larger)
import { z } from "@pithos/core/kanon/helpers/as-zod.shim";
const schema = z.object({ name: z.string() });

// Direct imports (smallest possible bundle)
import { object, string, parse } from "@pithos/core/kanon";
const schema = object({ name: string() });

Kanon pairs well with Zygos Result types for typed error handling: validate with Kanon, then wrap failures in typed Err values for explicit error propagation. See the practical example.


Related Resources