Runtime type validation with TypeScript inference. Great developer experience and high performance, with zero external dependencies.
TypeScript provides excellent compile-time type safety, but those types disappear at runtime. When data crosses boundaries (API responses, user input, file parsing), you need runtime validation that stays in sync with your TypeScript types.
Property Validator provides:
- Intuitive API — Fluent builder pattern that feels natural to write
- High Performance — JIT compilation for validation-heavy workloads
- Full Type Inference — TypeScript knows your validated types
- Zero Dependencies — Uses only Node.js standard library
- Three API Tiers — Choose your speed vs. detail trade-off
- Zero runtime dependencies (Node.js standard library only)
- TypeScript-first design with automatic type inference
- Framework-agnostic (works with React, Vue, Svelte, vanilla JS)
- Clear, actionable error messages
- Composable validators for complex types
- Works as CLI tool or library
Clone the repository:
git clone https://github.com/tuulbelt/property-validator.git
cd property-validator
npm install # Install dev dependencies onlyNo runtime dependencies — this tool uses only Node.js standard library.
CLI names — both short and long forms work:
- Short (recommended):
propval - Long:
property-validator
Recommended setup — install globally for easy access:
npm link # Enable the 'propval' command globally
propval --helpFor local development without global install:
npx tsx src/index.ts --helpFluent API (v namespace):
import { v, validate } from '@tuulbelt/property-validator';
// Define validators with built-in constraints
const userValidator = v.object({
name: v.string().min(1).max(100),
age: v.number().int().positive().max(150),
email: v.string().email()
});
// Validate data
const result = validate(userValidator, {
name: "Alice",
age: 30,
email: "alice@example.com"
});
if (result.ok) {
console.log(result.value); // Typed as { name: string, age: number, email: string }
} else {
console.error(result.error); // Clear error message
}Named imports (tree-shakeable):
// Import named validators from main entry point
import { validate, string, number, object, email, int, positive } from '@tuulbelt/property-validator';
const userValidator = object({
name: string(),
age: number(int(), positive()),
email: string(email())
});
const result = validate(userValidator, data);Using short name (recommended after npm link):
# Validate JSON from stdin
echo '{"name":"Alice","age":30}' | propval --schema user.schema.json
# Validate a file
propval --schema user.schema.json data.json
# Show help
propval --helpUsing long name:
property-validator --schema user.schema.json data.jsonProperty Validator supports tree-shaking for smaller bundle sizes. Import only what you need:
// Named exports (tree-shakeable)
import { string, number, object, validate } from '@tuulbelt/property-validator';
const userValidator = object({
name: string().min(1),
age: number().positive()
});
const result = validate(userValidator, data);Available named exports (main entry):
- Validators:
string,number,boolean,array,tuple,object,record,optional,nullable,union,discriminatedUnion,literal,lazy,enum_ - Functions:
validate,check,compile,compileCheck,toJsonSchema - Class:
ValidationError
Fluent API (v namespace from main entry):
// v namespace is available from the main entry point
import { v, validate } from '@tuulbelt/property-validator';
const schema = v.object({ name: v.string() });Type-only imports:
import type { Validator, Result, ValidationOptions } from '@tuulbelt/property-validator/types';Property Validator provides multiple entry points for different use cases:
| Entry Point | Import From | Use Case |
|---|---|---|
| Main | @tuulbelt/property-validator |
Full API (v namespace + named exports) |
| /types | @tuulbelt/property-validator/types |
Type definitions only |
Example: Main entry point (all APIs):
import { v, validate, check, string, number, object } from '@tuulbelt/property-validator';
// Fluent API with v namespace
const UserSchema = v.object({
name: v.string().email(),
age: v.number().positive()
});
// Or functional API with named exports
const AgeSchema = number(int(), positive());
const result = validate(UserSchema, data);Both styles from one import:
- Fluent API:
v.string().email()— Compact, chainable syntax - Functional API:
string(email())— Explicit imports, tree-shakeable refinements
For maximum tree-shaking potential, use the functional refinement pattern inspired by Valibot:
import { string, number, email, minLength, int, positive, validate } from '@tuulbelt/property-validator';
// Refinements are separate function exports - bundlers can exclude unused ones
const EmailSchema = string(email(), minLength(5));
const AgeSchema = number(int(), positive());
validate(EmailSchema, 'test@example.com'); // ✓
validate(AgeSchema, 25); // ✓Available refinement exports:
String refinements:
- Length:
minLength(n),maxLength(n),length(n),nonempty() - Format:
email(),url(),uuid(),pattern(regex, message?) - Content:
startsWith(prefix),endsWith(suffix),includes(substring) - Date/Time:
datetime(),date(),time() - Network:
ip(),ipv4(),ipv6()
Number refinements:
- Type:
int(),safeInt(),finite() - Sign:
positive(),negative(),nonnegative(),nonpositive() - Range:
min(n),max(n),range(min, max),multipleOf(n)
Array refinements:
- Length:
minItems(n),maxItems(n),itemCount(n),nonemptyArray()
Comparison: Chainable vs Functional API:
// Chainable API (compact, all methods bundled)
const schema1 = v.string().email().min(5);
// Functional API (tree-shakeable, explicit imports)
import { string, email, minLength } from '@tuulbelt/property-validator';
const schema2 = string(email(), minLength(5));
// Both produce equivalent validators!When to use which:
- Chainable API (
v.string().email()): Compact syntax, great for quick prototyping - Functional API (
string(email())): Maximum tree-shaking, explicit about what's used
| Import Style | Size (minified) | Size (gzipped) |
|---|---|---|
| Full bundle | 30 KB | 8 KB |
Why 30KB? — Performance-First Architecture
Property Validator prioritizes validation speed over bundle size. The bundle includes:
-
JIT Compilation Engine — Validators compile to optimized functions at schema definition time, not at first validation. This means zero startup cost when validating.
-
Pre-computed Fast Paths —
check()andcompileCheck()bypass error handling entirely, achieving sub-100ns validation for most schemas. -
Unified Validation Machinery — All validator types share optimized internals. This enables consistent ~60ns validation across primitives, objects, arrays, and unions.
The Trade-off:
| Approach | Bundle Size | Validation Speed |
|---|---|---|
| Minimal validators (lazy compilation) | ~15 KB | 200-500 ns (first call compiles) |
| Property Validator | 30 KB | 55-170 ns (pre-compiled) |
We chose speed. For applications validating API responses, form inputs, or processing data pipelines, the 15KB difference (~2KB gzipped) is negligible compared to React (~40KB), but the 3-5x speed improvement compounds across every validation call.
Tree-Shaking Benefits:
Named exports provide organizational clarity and some bundle reduction:
- Refinements are tree-shakeable — Only imported refinements like
email(),uuid(),minLength()are bundled - Type definitions excluded — Import from
/typesfor zero runtime cost - Code organization — Explicit imports make dependencies clear
Full validation with detailed error messages.
Parameters:
validator— Validator instance (created withv.*functions)data— Unknown data to validate
Returns:
Result<T>object with:ok: trueandvalue: Tif validation succeededok: falseanderror: ValidationErrorif validation failed
Best for: Form validation, API responses, anywhere you need error details.
const result = validate(UserSchema, data);
if (!result.ok) {
console.log(result.error.format('text')); // Human-readable error
}Fast boolean-only validation. Skips error path computation entirely.
Parameters:
validator— Validator instance (created withv.*functions)data— Unknown data to validate
Returns:
trueif valid,falseif invalid
Best for: Conditionals, filtering, type guards, anywhere you only need pass/fail.
import { v, check } from '@tuulbelt/property-validator';
const UserSchema = v.object({ name: v.string(), age: v.number() });
// Use in conditionals
if (check(UserSchema, data)) {
processUser(data); // data is valid
}
// Use for filtering
const validUsers = users.filter(u => check(UserSchema, u));Pre-compile a validator for maximum-speed boolean validation. Returns a cached function.
Parameters:
validator— Validator instance to compile
Returns:
(data: unknown) => boolean— Compiled check function
Best for: Hot paths, large datasets, performance-critical loops.
import { v, compileCheck } from '@tuulbelt/property-validator';
const UserSchema = v.object({ name: v.string(), age: v.number() });
const isValidUser = compileCheck(UserSchema); // Compile once
// Use in hot loops (maximum speed)
for (const user of users) {
if (isValidUser(user)) {
processUser(user);
}
}| Use Case | API | Why |
|---|---|---|
| Form validation | validate() |
Need error messages for UX |
| API request validation | validate() |
Need detailed errors for debugging |
| Type guards / conditionals | check() |
Simple pass/fail, faster |
| Filtering arrays | check() |
Boolean predicate needed |
| High-throughput pipelines | compileCheck() |
Maximum speed, pre-compiled |
| Validating same schema 1000+ times | compileCheck() |
Compilation overhead amortized |
Primitives:
v.string()— String validatorv.number()— Number validatorv.boolean()— Boolean validator
Built-in String Constraints:
.min(n)/.max(n)/.length(n)— Length constraints.nonempty()— Requires non-empty string.email()— Valid email address.url()— Valid HTTP/HTTPS URL.uuid()— Valid UUID (v1-v5).pattern(regex, message?)— Custom regex pattern.startsWith(prefix)/.endsWith(suffix)/.includes(substring)
Built-in Number Constraints:
.int()— Integer only.positive()/.negative()— Sign constraints (exclusive).nonnegative()/.nonpositive()— Sign constraints (inclusive).min(n)/.max(n)— Value bounds.range(min, max)— Value range (inclusive).finite()— Not Infinity or NaN.safeInt()— Safe integer range
Collections:
v.array(itemValidator)— Array validator (homogeneous elements).min(n)— Minimum length constraint.max(n)— Maximum length constraint.length(n)— Exact length constraint.nonempty()— Requires at least 1 element
v.tuple([...validators])— Tuple validator (fixed-length, per-index types)
Objects:
v.object(shape)— Object validator with shape.strict()— Reject objects with unknown properties (v0.10.0+).passthrough()— Allow unknown properties in output (v0.10.0+)
v.record(keyValidator, valueValidator)— Dynamic key-value pairs (v0.10.0+)
Unions and Literals:
v.union([validator1, validator2, ...])— Union validator (OR logic, validates if any schema matches)v.discriminatedUnion(discriminator, variants)— Efficient tagged unions (v0.10.0+)v.literal(value)— Literal validator (exact value matching using===)v.enum(['a', 'b', 'c'])— Enum validator (union of string literals)
Modifiers:
v.optional(validator)— Optional field (allows undefined) [deprecated: use.optional()method]v.nullable(validator)— Nullable field (allows null) [deprecated: use.nullable()method]
Chainable Methods (all validators):
.refine(predicate, message)— Add custom validation logic.transform(fn)— Transform validated value (changes type).optional()— Allow undefined.nullable()— Allow null.nullish()— Allow undefined or null.default(value)— Provide default value (static or lazy function)
// Basic array validation
const numbersValidator = v.array(v.number());
validate(numbersValidator, [1, 2, 3]); // ✓
// Array with length constraints
const tagsValidator = v.array(v.string()).min(1).max(5);
validate(tagsValidator, ['typescript', 'validation']); // ✓
// Array of objects
const usersValidator = v.array(v.object({
name: v.string(),
age: v.number()
}));
validate(usersValidator, [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 }
]); // ✓
// Nested arrays (2D matrix)
const matrixValidator = v.array(v.array(v.number()));
validate(matrixValidator, [[1, 2], [3, 4]]); // ✓// Coordinate tuple [x, y]
const coordValidator = v.tuple([v.number(), v.number()]);
validate(coordValidator, [10, 20]); // ✓
// Mixed-type tuple
const personValidator = v.tuple([
v.string(), // name
v.number(), // age
v.boolean() // active
]);
validate(personValidator, ['Alice', 30, true]); // ✓
// Tuple with optional field
const entryValidator = v.tuple([
v.string(),
v.optional(v.number())
]);
validate(entryValidator, ['key', undefined]); // ✓
validate(entryValidator, ['key', 42]); // ✓// Simple union (string | number)
const stringOrNumber = v.union([v.string(), v.number()]);
validate(stringOrNumber, 'hello'); // ✓
validate(stringOrNumber, 42); // ✓
validate(stringOrNumber, true); // ✗
// Discriminated unions (tagged unions)
const apiResponse = v.union([
v.object({ type: v.literal('success'), data: v.string() }),
v.object({ type: v.literal('error'), message: v.string() })
]);
validate(apiResponse, { type: 'success', data: 'OK' }); // ✓
validate(apiResponse, { type: 'error', message: 'Failed' }); // ✓
// Enum as union sugar
const statusValidator = v.enum(['active', 'inactive', 'pending']);
validate(statusValidator, 'active'); // ✓
validate(statusValidator, 'archived'); // ✗// Efficient tagged unions with discriminator field
const apiResponse = v.discriminatedUnion('type', {
success: v.object({ type: v.literal('success'), data: v.string() }),
error: v.object({ type: v.literal('error'), code: v.number(), message: v.string() }),
pending: v.object({ type: v.literal('pending'), retryAfter: v.number() })
});
// O(1) lookup by discriminator value (faster than v.union() for many variants)
validate(apiResponse, { type: 'success', data: 'OK' }); // ✓
validate(apiResponse, { type: 'error', code: 404, message: 'Not found' }); // ✓
validate(apiResponse, { type: 'unknown' }); // ✗ "Unknown discriminator value: unknown"// Dynamic key-value pairs
const scores = v.record(v.string(), v.number());
validate(scores, { alice: 100, bob: 85 }); // ✓
validate(scores, { alice: 'A' }); // ✗
// Record with key constraints
const uuidMap = v.record(v.string().uuid(), v.object({ name: v.string() }));
validate(uuidMap, { '550e8400-e29b-41d4-a716-446655440000': { name: 'Item' } }); // ✓// Default: unknown properties are silently ignored
const user = v.object({ name: v.string() });
validate(user, { name: 'Alice', extra: true }); // ✓ (extra ignored)
// Strict: reject unknown properties
const strictUser = v.object({ name: v.string() }).strict();
validate(strictUser, { name: 'Alice' }); // ✓
validate(strictUser, { name: 'Alice', extra: true }); // ✗ "Unknown key: extra"
// Passthrough: preserve unknown properties in output
const passthroughUser = v.object({ name: v.string() }).passthrough();
const result = validate(passthroughUser, { name: 'Alice', extra: true });
// result.value = { name: 'Alice', extra: true } (extra preserved)// Email validation (built-in)
const email = v.string().email();
validate(email, 'alice@example.com'); // ✓
validate(email, 'not-an-email'); // ✗ "Must be a valid email address"
// URL validation (built-in)
const website = v.string().url();
validate(website, 'https://example.com'); // ✓
// Number constraints (built-in)
const age = v.number().int().positive().max(150);
validate(age, 25); // ✓
validate(age, -5); // ✗ "Number must be positive"
validate(age, 25.5); // ✗ "Number must be an integer"
// String constraints (built-in)
const username = v.string().min(3).max(20).pattern(/^[a-z0-9_]+$/);
validate(username, 'john_doe'); // ✓
validate(username, 'ab'); // ✗ "String must be at least 3 character(s)"// Custom validation with .refine()
const password = v.string()
.min(8) // Built-in length check
.refine(s => /[A-Z]/.test(s), 'Must contain uppercase letter')
.refine(s => /[0-9]/.test(s), 'Must contain number');
validate(password, 'SecurePass123'); // ✓
validate(password, 'weak'); // ✗ "String must be at least 8 character(s)"// Parse string to integer
const parsedInt = v.string().transform(s => parseInt(s, 10));
const result = validate(parsedInt, '42');
if (result.ok) {
console.log(result.value); // 42 (number)
}
// Trim and lowercase
const normalized = v.string()
.transform(s => s.trim())
.transform(s => s.toLowerCase());
validate(normalized, ' HELLO '); // ✓ value: "hello"
// Transform with refinement
const positiveInt = v.string()
.transform(s => parseInt(s, 10))
.refine(n => n > 0, 'Must be positive integer');
validate(positiveInt, '42'); // ✓ value: 42
validate(positiveInt, '-5'); // ✗ "Must be positive integer"// Optional field (allows undefined)
const optionalString = v.string().optional();
validate(optionalString, 'hello'); // ✓
validate(optionalString, undefined); // ✓
validate(optionalString, null); // ✗
// Nullable field (allows null)
const nullableNumber = v.number().nullable();
validate(nullableNumber, 42); // ✓
validate(nullableNumber, null); // ✓
validate(nullableNumber, undefined); // ✗
// Nullish (allows both undefined and null)
const nullishBoolean = v.boolean().nullish();
validate(nullishBoolean, true); // ✓
validate(nullishBoolean, undefined); // ✓
validate(nullishBoolean, null); // ✓
// Static default value
const withDefault = v.string().default('default-value');
validate(withDefault, 'custom'); // ✓ value: "custom"
validate(withDefault, undefined); // ✓ value: "default-value"
// Lazy default (function called each time)
const withTimestamp = v.number().default(() => Date.now());
validate(withTimestamp, undefined); // ✓ value: current timestamp
// Config with defaults
const configValidator = v.object({
port: v.number().default(3000),
host: v.string().default('localhost'),
debug: v.boolean().default(false)
});
validate(configValidator, { port: undefined, host: undefined, debug: undefined });
// ✓ value: { port: 3000, host: "localhost", debug: false }import { v } from '@tuulbelt/property-validator';
import type { Validator } from '@tuulbelt/property-validator/types';
// Create custom validator
const emailValidator: Validator<string> = {
validate(data: unknown): data is string {
return typeof data === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data);
},
error(data: unknown): string {
return `Expected email, got: ${typeof data}`;
}
};
// Use in object schema
const userValidator = v.object({
email: emailValidator
});See the examples/ directory for runnable examples:
npx tsx examples/basic.tsnpm test # Run all tests
npm test -- --watch # Watch modeTuulbelt tools validate each other via devDependencies. This tool uses:
- test-flakiness-detector to validate test determinism
- output-diffing-utility to verify validation output is deterministic
How It Works:
npm run dogfood # Runs both flaky detection + output diff validation
npm run dogfood:flaky # Runs tests 10 times to catch flaky tests
npm run dogfood:diff # Validates output determinism via diffThis runs automatically in CI on every push/PR.
Why This Matters:
- Validation must be deterministic (same input → same output, every time)
- test-flakiness-detector ensures tests don't randomly fail
- output-diffing-utility proves validation produces consistent results
- Critical for caching, reproducible builds, and reliable testing
Configuration (package.json):
{
"scripts": {
"dogfood": "npm run dogfood:flaky && npm run dogfood:diff",
"dogfood:flaky": "flaky --test 'npm test' --runs 10",
"dogfood:diff": "bash scripts/dogfood-diff.sh"
},
"devDependencies": {
"@tuulbelt/test-flakiness-detector": "git+https://github.com/tuulbelt/test-flakiness-detector.git",
"@tuulbelt/output-diffing-utility": "git+https://github.com/tuulbelt/output-diffing-utility.git"
}
}See DOGFOODING_STRATEGY.md for the decision tree on when to add additional Tuulbelt tools as devDependencies.
Exit codes:
0— Success1— Error (invalid input, validation failure)
Errors are returned in the error field of the result object, not thrown.
Property Validator is built for high-throughput validation with zero runtime dependencies. It offers three API tiers to match your performance needs.
| API | Speed | Returns | Best For |
|---|---|---|---|
validate() |
~170 ns | Result<T> with errors |
Forms, APIs, debugging |
check() |
~60 ns | boolean |
Filtering, conditionals |
compileCheck() |
~55 ns | boolean (pre-compiled) |
Hot paths, pipelines |
Performance comparison across API tiers for common validation scenarios:
| Scenario | validate() | check() | compileCheck() |
|---|---|---|---|
| Simple Object | 170 ns | 58 ns | 55 ns |
| Complex Nested | 190 ns | 60 ns | 58 ns |
| Array (10 items) | 250 ns | 65 ns | 63 ns |
| Array (100 items) | 2.1 µs | 145 ns | 140 ns |
| Union (3 types) | 90 ns | 66 ns | 56 ns |
Key Insights:
check()is ~3x faster thanvalidate()for valid datacompileCheck()adds another 5-15% on top ofcheck()- The gap is largest for arrays and complex objects
- For invalid data,
check()is 6x+ faster (skips error path entirely)
Property Validator uses JIT (Just-In-Time) compilation to optimize validation:
- Schema Definition — You define schemas with the fluent builder API
- Automatic JIT — On first validation, schemas compile to optimized functions
- Fast Path —
check()andcompileCheck()bypass error handling entirely
// Behind the scenes, this:
const UserSchema = v.object({ name: v.string(), age: v.number() });
// Compiles to something like:
const compiled = (data) =>
typeof data === 'object' && data !== null &&
typeof data.name === 'string' &&
typeof data.age === 'number';For comprehensive benchmarks including comparisons with other libraries, see benchmarks/README.md.
Benchmarks use tatami-ng with criterion-equivalent statistical rigor (~1% variance).
Convert property-validator schemas to JSON Schema Draft 7 for OpenAPI compatibility:
import { v, toJsonSchema } from '@tuulbelt/property-validator';
const UserSchema = v.object({
name: v.string().min(1),
age: v.number().int().positive(),
email: v.optional(v.string().email()),
role: v.union([v.literal('admin'), v.literal('user')])
});
const jsonSchema = toJsonSchema(UserSchema);
// {
// "$schema": "http://json-schema.org/draft-07/schema#",
// "type": "object",
// "properties": {
// "name": { "type": "string", "minLength": 1 },
// "age": { "type": "number" },
// "email": { "type": "string", "format": "email" },
// "role": { "enum": ["admin", "user"] }
// },
// "required": ["name", "age", "role"]
// }Options:
toJsonSchema(schema, {
includeSchema: true, // Include $schema declaration (default: true)
draft: 'http://json-schema.org/draft-07/schema#', // JSON Schema draft
unknownTypeHandling: 'any', // 'any' | 'throw' | 'empty'
includeMetadata: false // Include title/description if available
});Use Cases:
- OpenAPI/Swagger — Generate API documentation from your validators
- Form Generation — Auto-generate forms from schemas
- Interoperability — Share schemas with other tools/languages
- Documentation — Self-documenting schemas
If you're migrating from another validation library, see MIGRATION.md for a complete guide with side-by-side examples and API comparisons.
- Schema generation from existing TypeScript types
- Async validators for database/API checks
- Intersection types
- Streaming validation for large files
v0.10.0:
- ✅ JSON Schema Export —
toJsonSchema()converts schemas to JSON Schema Draft 7 - ✅ Full Modularization — Validators extracted to individual modules (index.ts: 3744→149 lines)
- ✅
record()validator — Dynamic keys withv.record(keyValidator, valueValidator) - ✅
discriminatedUnion()— Efficient tagged unions with discriminator field - ✅
strict()/passthrough()— Control unknown property handling - ✅ Extended String Validators —
cuid(),cuid2(),ulid(),nanoid(),base64(),hex(),jwt() - ✅ Extended Number Validators —
port(),latitude(),longitude(),percentage() - ✅ Array JIT for Objects — Optimized array-of-object validation
- ✅ 898 tests — Comprehensive test coverage
v0.9.1:
- ✅ Functional refinement API —
string(email(), minLength(5))pattern - ✅ Tree-shakeable refinements — 32 refinement functions as separate exports
- ✅ Backwards compatible — Chainable API still works unchanged
- ✅ 44 new tests — Full test coverage for functional API
v0.9.0:
- ✅ Modular architecture — Tree-shakeable named exports
- ✅ Separate types module —
@tuulbelt/property-validator/types - ✅ Package.json exports — Proper bundler support
- ✅ sideEffects: false — Bundle optimization enabled
v0.8.5:
- ✅
check()API — Boolean-only validation (~3x faster than validate) - ✅
compileCheck()API — Pre-compiled for hot paths - ✅ Built-in string validators (email, url, uuid, pattern, etc.)
- ✅ Built-in number validators (int, positive, negative, range, etc.)
- ✅ Restructured benchmarks with API equivalence methodology
v0.8.0:
- ✅ JIT bypass pattern — Direct access to compiled functions
- ✅ Recursive JIT bypass for nested objects (20x faster)
- ✅ JIT bypass for arrays, unions, primitives, literals
v0.7.5:
- ✅ Pre-compiled validators with fast-path optimization
- ✅ Lazy path building (paths computed only on errors)
- ✅ tatami-ng benchmarking with criterion-equivalent rigor
v0.4.0:
- ✅ Schema compilation (
v.compile()) with automatic caching - ✅ Error formatting (
.format('json'),.format('text'),.format('color')) - ✅ Circular reference detection (
v.lazy()) - ✅ Security limits (
maxDepth,maxProperties,maxItems)
▶ View interactive recording on asciinema.org
Demos are automatically generated and embedded via GitHub Actions when demo scripts are updated.
MIT — see LICENSE
See CONTRIBUTING.md for contribution guidelines.
Part of the Tuulbelt collection:
- Test Flakiness Detector — Detect unreliable tests
- CLI Progress Reporting — Concurrent-safe progress updates
- More tools coming soon...
Property Validator uses a multi-tier optimization strategy:
- Schema Compilation — Validators compile to optimized functions at definition time
- JIT Fast Path —
check()andcompileCheck()bypass error handling entirely - Lazy Error Paths — Error paths only computed when validation fails
Property Validator prioritizes:
- Detailed Error Paths — Full paths like
users[2].metadata.tags[0] - Circular Reference Detection — Prevents infinite loops in recursive schemas
- Security Limits — DoS protection via
maxDepth,maxProperties,maxItems - Zero Dependencies — Uses only Node.js standard library
These features add overhead compared to minimal validators, which is why we offer three API tiers:
| Need | Use |
|---|---|
| Error messages for users | validate() |
| Fast pass/fail checks | check() |
| Maximum throughput | compileCheck() |
cd benchmarks
npm install
npm run bench # Internal API comparison
npm run bench:compare # Full comparison including competitorsSee benchmarks/README.md for methodology and results.
