Kanon V3 - Complete Features
Overviewโ
Kanon V3 is a TypeScript validation library with a pure functional architecture, optimized for performance with fast paths, singleton pattern, and early abort.
Core Principle: Pure Validation, No Transformationโ
Kanon V3 validates data but does not transform it. Upon successful validation, data is returned as-is, without modification. "Transform" functions (partial, pick, omit, required, keyof) transform the validation schema structure, not the data itself.
Validation: Verifies that data matches the schema
Transformation: Does not modify data (no .transform(),.preprocess(),.trim(),.toLowerCase(), etc.)- ๐ Coercion:
coerce*functions convert the type before validation, but don't modify the structure of validated data
This is a deliberate architectural choice. Validation and transformation are different concerns that should be handled separately. See Why No Transformations? for the full rationale.
Primitive Typesโ
Basic Typesโ
string(message?)- String validationnumber(message?)- Number validationint(message?)- Integer validationboolean(message?)- Boolean validationnull_(message?)- Null value validationundefined_(message?)- Undefined value validationbigint(message?)- BigInt validationdate(message?)- Date validationsymbol(message?)- Symbol validationany(message?)- Accepts any type (message ignored but accepted for API consistency)unknown(message?)- Unknown type (safe, message ignored but accepted for API consistency)never(message?)- Type that never accepts a valuevoid_(message?)- Void type
String Constraintsโ
Format Validationsโ
.minLength(min, errorMessage?)- Minimum length.maxLength(max, errorMessage?)- Maximum length.length(length, errorMessage?)- Exact length.email(errorMessage?)- Email validation (regex).url(errorMessage?)- URL validation (regex).uuid(errorMessage?)- UUID validation (regex).pattern(regex, errorMessage?)- Regular expression validation.includes(substring, errorMessage?)- Contains substring.startsWith(prefix, errorMessage?)- Starts with prefix.endsWith(suffix, errorMessage?)- Ends with suffix
Number Constraintsโ
.min(minValue, errorMessage?)- Minimum value.max(maxValue, errorMessage?)- Maximum value.int(errorMessage?)- Integer number.positive(errorMessage?)- Strictly positive number.negative(errorMessage?)- Strictly negative number.lt(lessThan, errorMessage?)- Less than (strictly).lte(lessThanOrEqual, errorMessage?)- Less than or equal.gt(greaterThan, errorMessage?)- Greater than (strictly).gte(greaterThanOrEqual, errorMessage?)- Greater than or equal.multipleOf(multiple, errorMessage?)- Multiple of a number
Array Constraintsโ
.minLength(min, errorMessage?)- Minimum length.maxLength(max, errorMessage?)- Maximum length.length(length, errorMessage?)- Exact length.unique(errorMessage?)- Unique elements (no duplicates)
Date Constraintsโ
.min(minDate, errorMessage?)- Minimum date.max(maxDate, errorMessage?)- Maximum date.before(beforeDate, errorMessage?)- Before a date.after(afterDate, errorMessage?)- After a date
BigInt Constraintsโ
.min(minValue, errorMessage?)- Minimum value.max(maxValue, errorMessage?)- Maximum value.positive(errorMessage?)- Strictly positive BigInt.negative(errorMessage?)- Strictly negative BigInt
Object Constraintsโ
.minKeys(min, errorMessage?)- Minimum number of keys.maxKeys(max, errorMessage?)- Maximum number of keys.strict(errorMessage?)- Validates that the object contains only properties defined in the schema (rejects additional properties)
Composite Typesโ
Collectionsโ
-
array(itemSchema, message?)- Array of validated elements.minLength(min, errorMessage?)- Minimum length.maxLength(max, errorMessage?)- Maximum length.length(length, errorMessage?)- Exact length.unique(errorMessage?)- Unique elements
-
tuple(schemas, message?)- Typed tuple with schemas for each position- Variants:
tupleOf(schema1, schema2, message?),tupleOf3(schema1, schema2, schema3, message?),tupleOf4(schema1, schema2, schema3, schema4, message?) tupleWithRest(schemas, restSchema, message?)- Tuple with rest schema for variadic tuples
- Variants:
-
record(keySchema, valueSchema, message?)- Object with validated keys and values -
map(keySchema, valueSchema, message?)- Map with validated keys and values.minSize(min, errorMessage?)- Minimum size.maxSize(max, errorMessage?)- Maximum size
-
set(itemSchema, message?)- Set with validated elements.minSize(min, errorMessage?)- Minimum size.maxSize(max, errorMessage?)- Maximum size
Objectsโ
-
object(shape, message?)- Object with defined schema- Validates each property according to its schema
- Support for optional properties via
optional(schema)on the property schema
-
strictObject(shape, message?)- Strict object (rejects additional properties)Note:
strictObject()andobject().strict()produce the same behavior. UsestrictObject()to directly create a strict object, orobject().strict()for method chaining. -
looseObject(shape, message?)- Permissive object (accepts additional properties)
Object Manipulation (Schema Transformations)โ
These functions transform the validation schema structure, not the data itself. They allow creating new schemas from existing schemas. Validated data is returned as-is, without transformation.
Important: These functions transform the schema, not the data. For example, pick(schema, ['name']) validates only the name property, but returns the complete object with all its properties if validation succeeds.
-
partial(objectSchema, message?)- Makes all properties optional -
required(objectSchema, message?)- Makes all properties required -
pick(objectSchema, keys, message?)- Selects certain properties -
omit(objectSchema, keys, message?)- Excludes certain properties -
keyof(objectSchema, message?)- Keys of an object schema- Automatically extracts keys from the object schema passed as parameter
- Type-safe: preserves the type of object keys (
keyof T & string)
Unions and Intersectionsโ
-
unionOf(schema1, schema2, message?)- Union of two schemas (OR)- Variants:
unionOf3(schema1, schema2, schema3, message?),unionOf4(schema1, schema2, schema3, schema4, message?) - Note: For more than two schemas, use typed variants or create multiple nested unions
- Variants:
-
intersection(schema1, schema2, message?)- Intersection of two schemas (AND)- Variant:
intersection3(schema1, schema2, schema3, message?)for three schemas
- Variant:
Special Typesโ
-
literal(value, message?)- Exact literal value (string, number, boolean, null) -
enum_(values, message?)- String enum- Variants:
numberEnum(values, message?),booleanEnum(values, message?),mixedEnum(values, message?)
- Variants:
-
nativeEnum(enumObj, message?)- Native TypeScript enum
Advantages Over Zodโ
Kanon offers specialized functions for number, boolean, and mixed enums, which is more concise and performant than Zod:
In Zod (more verbose):
// Number enum
const status = z.union([z.literal(100), z.literal(200), z.literal(300)]);
// Boolean enum
const flag = z.union([z.literal(true), z.literal(false)]);
// Mixed enum
const value = z.union([z.literal("red"), z.literal(42), z.literal(true)]);
In Kanon (more concise and expressive):
// Number enum - More concise!
const status = numberEnum([100, 200, 300] as const);
// Boolean enum - More concise!
const flag = booleanEnum([true, false] as const);
// Mixed enum - More concise!
const value = mixedEnum(["red", 42, true] as const);
Kanon Advantages:
More concise: numberEnum([1, 2, 3])vsz.union([z.literal(1), z.literal(2), z.literal(3)])
Better type inference: TypeScript directly infers 1 | 2 | 3without going throughz.infer
Dedicated API: Specialized functions instead of generic composition
Optimized performance: Implementation via shared and optimized createEnumSchema()
Wrappersโ
Nullability Modifiersโ
-
optional(schema)- Makes the schema optional (acceptsundefined) -
nullable(schema, message?)- Makes the schema nullable (acceptsnull) -
default_(schema, defaultValue, message?)- Default value if missing- Support for function for dynamic default value
- Helper:
DefaultValuesfor common default values
Other Wrappersโ
-
readonly(schema, message?)- Marks as readonly -
lazy(factory, message?)- Lazy schema (lazy evaluation) for circular references
Refinementsโ
Refinements are used internally by constraints (.minLength(), .email(), etc.). Constraints automatically add refinements to the schema via the refineString(), refineNumber(), refineArray(), refineObject(), refineDate(), refineBigInt() functions.
Schemas support a refinements property that stores custom validations, but there is no public .refine() method for direct chaining.
Coercion (Automatic Conversion)โ
Coercion functions convert the input type before validation, but don't modify the structure of validated data. They are useful for accepting flexible formats (e.g., string "123" โ number 123).
coerceString(message?)- Coerce to stringcoerceNumber(message?)- Coerce to numbercoerceBoolean()- Coerce to boolean (no message parameter)coerceBigInt(message?)- Coerce to bigintcoerceDate(message?)- Coerce to date
Note: Coercion converts the type, but the data returned after validation is the converted data, not transformed (no structure modification, no normalization).
Parsingโ
Synchronous Methodsโ
parse(schema, input)- Parse and return{ success: true, data: T } | { success: false, error: string }
Bulk Validationโ
-
parseBulk(schema, values, options?)- Bulk validationearlyAbortoption: stops at first error (fast mode)- Without
earlyAbort: collects all errors (complete mode) - Returns
{ success: true, data: T[] } | { success: false, errors: string[] | string }
Error Handlingโ
Error Structureโ
V3 uses a simplified error system:
- Error messages as
string(no complex objects) - Constant messages to optimize performance
- Customizable messages via the
errorMessage?parameter of each constraint
Advantagesโ
- Performance: No allocation of complex error objects
- Simplicity: Directly readable error messages
- Flexibility: Customizable messages per constraint
Type Inferenceโ
Utility Typesโ
TypeScript types are automatically inferred from schemas:
- Automatic output type inference
- TypeScript extensions for fluent API
- Specialized types for each constraint (
StringConstraint,NumberConstraint, etc.)
Chaining APIโ
Schemas with constraints support method chaining for constraints:
const schema = string().minLength(5).maxLength(100).email();
Note: Wrappers (optional(), nullable(), default_()) are separate functions, not chaining methods:
const schema = optional(
default_(string().minLength(5).email(), "default@example.com")
);
Usage Examplesโ
Simple Schemaโ
import { string, number, object, optional } from "@kanon";
const userSchema = object({
name: string().minLength(1),
age: number().min(0).int(),
email: string().email(),
phone: optional(string()), // Optional property
});
Strict Schema (Rejects Additional Properties)โ
import { string, number, object, strictObject } from "@kanon";
// Method 1: Use .strict() to make an object strict (chaining)
const strictSchema = object({
name: string(),
age: number(),
}).strict();
// Method 2: Use strictObject() directly (equivalent)
const strictSchema2 = strictObject({
name: string(),
age: number(),
});
// Both produce the same behavior: reject additional properties
parse(strictSchema, { name: "John", age: 30, extra: "value" }); // โ Error
parse(strictSchema2, { name: "John", age: 30, extra: "value" }); // โ Error
// Use strictObject() to directly create a strict object,
// or object().strict() if you need to chain other methods before
const strictWithConstraints = object({
name: string(),
age: number(),
}).minKeys(1).strict();
Complex Schemaโ
import { string, number, object, array, record, unionOf } from "@kanon";
const complexSchema = object({
id: string().uuid(),
profile: object({
firstName: string().minLength(1),
lastName: string().minLength(1),
}),
tags: array(string()).minLength(1),
metadata: record(string(), unionOf(string(), number())),
});
Union and Intersectionโ
import { string, number, unionOf, intersection, object } from "@kanon";
const stringOrNumber = unionOf(string(), number());
const userWithId = intersection(
object({ id: string() }),
object({ name: string() })
);
Lazy Evaluationโ
import { string, array, object, lazy } from "@kanon";
type Node = {
value: string;
children: Node[];
};
const nodeSchema = lazy(() =>
object({
value: string(),
children: array(nodeSchema),
})
);
Bulk Validation with Early Abortโ
import { string, parseBulk } from "@kanon";
const schema = string().email();
const emails = ["valid@example.com", "invalid", "another@example.com"];
// Fast mode: stops at first error
const result = parseBulk(schema, emails, { earlyAbort: true });
if (!result.success) {
console.log(result.errors); // "Index 1: Invalid email format"
}
Architectureโ
Pure Functional Patternโ
V3 uses pure functions for each schema type:
- No classes, only functions
- Validation via
validator: (value: unknown) => true | string - Simple composition via TypeScript extensions
Performance Optimizationsโ
- Fast paths: Explicit optimizations for common cases
- Singleton pattern: Reduction of memory allocations
- Early abort: Immediate stop on first error in bulk validation
- Constant messages: Avoids string interpolation on each validation
- Inlining: Functions marked
/*@__INLINE__*/for compiler optimization
Schema Structureโ
Each schema exposes:
type: SchemaType- Validation type (e.g., "string", "array", "object")message?: string- Optional custom error messagerefinements?: Array<(value: T) => true | string>- Custom validationsvalidator: (value: unknown) => true | string- Validation function- Composition properties depending on type (
entries,item,schemas,keySchema,valueSchema,itemSchema, etc.)
Fluent Extensionsโ
Constraints are added via TypeScript extensions:
StringSchema & StringExtensionโStringConstraintNumberSchema & NumberExtensionโNumberConstraintArraySchema & ArrayExtensionโArrayConstraint- etc.
This enables a fluent API with complete TypeScript autocompletion.
Known Limitationsโ
Unsupported Featuresโ
- Async parsing: No native support for
parseAsync()orsafeParseAsync(). Useparse()andparseBulk()which are synchronous but optimized. - Nested error path: Error messages don't contain structured error path (no
patharray). Errors are simple strings. - String transformations: No
.toLowerCase(),.toUpperCase(),.trim()methods like in V1. Use custom refinements if needed.
Differences with V1โ
Architectureโ
- V1: Classes with mutable chaining
- V3: Pure functions with TypeScript extensions
Error Handlingโ
- V1: Complex
PithosIssueobjects with codes and paths - V3: Simple error messages (
string)
Performanceโ
- V1: Baseline
- V3: +200% vs V1 thanks to fast paths and singleton pattern
Parsingโ
- V1:
parse(),safeParse(),parseAsync(),safeParseAsync() - V3:
parse()andparseBulk()(no native async support, but early abort to optimize)
Flexibilityโ
- V1: Complete fluent API but more rigid architecture
- V3: Fluent API with natural composition and extensibility via TypeScript extensions
Wrappersโ
- V1: Chaining methods (
.optional(),.nullable(),.default()) - V3: Separate functions (
optional(),nullable(),default_()) that take a schema as parameter
Next Stepsโ
- Architecture & Evolution - Learn about the V1โV2โV3 evolution
- Design Innovations - Explore theoretical evolutions and why they were abandoned
- API Reference - Detailed API documentation