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