The world's fastest and most accurate json-schema validator, with magical typescript inference.
100% spec compliance. 51% faster than ajv. Zero dependencies. Full TypeScript inference.
| tjs | ajv | zod | joi | |
|---|---|---|---|---|
| JSON Schema compliance | 100% | 95% | Basic | None |
| TypeScript inference | Built-in | Plugin | Built-in | None |
| Dependencies | 0 | 4+ | 0 | 5+ |
| Performance | Fastest | Fast | Slow | Slow |
npm install tjsimport { schema } from 'tjs';
const User = schema({
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 },
},
required: ['name', 'email'],
});
type User = typeof User.type; // Extract the type
// Validate with full type inference
const user = User.assert(data);
// ^? { name: string; email: string; age?: number }- Bridge the gap between json-schema and TypeScript types with no code duplication.
- Types are guaranteed to be correct at compile time and run time.
- Sticking to json-schema means your schema can be published to third parties with no translation layer.
tjs passes the entire JSON Schema Test Suite — the official compliance benchmark:
| Draft | Compliance |
|---|---|
| draft-04 | 100% (882/882) |
| draft-06 | 100% (1170/1170) |
| draft-07 | 100% (1534/1534) |
| draft-2019-09 | 100% (1941/1941) |
| draft-2020-12 | 100% (1990/1990) |
| Total | 100% (7517/7517) |
See COMPLIANCE.md for details.
See BENCHMARKS.md for detailed performance comparison. tjs uses JIT compilation to generate optimized validation code — 51% faster than ajv overall:
Performance vs ajv (JSON Schema Test Suite):
--------------------------------------------------------------------------------
Draft Files Tests | tjs ns/test ajv ns/test Diff
--------------------------------------------------------------------------------
draft-04 43 881 | 35 74 -53%
draft-06 52 1170 | 34 66 -49%
draft-07 63 1534 | 61 75 -18%
draft-2019-09 77 1941 | 51 151 -66%
draft-2020-12 80 1990 | 50 147 -66%
--------------------------------------------------------------------------------
TOTAL 315 7516 | 48 98 -51%
--------------------------------------------------------------------------------
Format validation is where tjs really shines — up to 238x faster for complex formats:
idn-email 238x faster than ajv
date-time 9x faster than ajv
ipv6 6x faster than ajv
tjs achieves its performance through several code generation optimizations that ajv doesn't implement:
| Optimization | tjs | ajv |
|---|---|---|
| Format validators | Character-code parsing with lookup tables | Complex regex patterns (800+ chars for ipv6) |
| typeof caching | Caches typeof result once for multi-type checks |
Calls typeof repeatedly |
| oneOf early exit | Returns immediately when 2nd match found | Evaluates all branches, tracks matches in arrays |
| Error allocation | Pre-compiled error objects, zero allocation in hot path | Creates error objects inline during validation |
The format validators show the largest gains. For example, date-time validation in tjs uses direct character code comparisons:
// tjs: ~140 lines of simple character code checks
if (s.charCodeAt(4) !== 45) return false; // '-'
const year = (s.charCodeAt(0) - 48) * 1000 + ...vs ajv-formats which uses regex with potential backtracking:
// ajv-formats: regex pattern
/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/itjs infers TypeScript types directly from your schema — no code generation, no separate type definitions:
import { schema } from 'tjs';
const Product = schema({
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
price: { type: 'number', minimum: 0 },
tags: { type: 'array', items: { type: 'string' } },
status: { enum: ['active', 'discontinued'] },
},
required: ['id', 'name', 'price'],
});
// Extract the inferred type
type Product = typeof Product.type;
// { id: string; name: string; price: number; tags?: string[]; status?: 'active' | 'discontinued' }Types are inferred for:
- All primitive types (
string,number,integer,boolean,null) - Arrays with
itemsandprefixItems(tuples) - Objects with
properties,additionalProperties,patternProperties - Union types via
anyOf,oneOf - Intersection types via
allOf - Const and enum literals
- Conditional schemas with
if/then/else - Recursive schemas with
$refand$defs
tjs has zero runtime dependencies. The entire library is ~25KB minified.
Compare to ajv which requires:
fast-deep-equaljson-schema-traverserequire-from-stringuri-js(which itself has dependencies)
Create a validator from a JSON Schema:
const User = schema({
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
},
required: ['name'],
});Validate and return result with value or error:
const result = User.validate(input);
if (result.error === undefined) {
console.log(result.value); // Typed & coerced data
} else {
console.log(result.error); // Validation errors with paths
}Assert validity, throwing on failure. Returns coerced value:
try {
const user = User.assert(input); // Returns typed, coerced data
console.log(user.name);
} catch (e) {
console.error('Invalid:', e.message);
}Ergonomic helper for object schemas:
import { struct } from 'tjs';
const User = struct({
id: 'string',
name: 'string',
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, optional: true },
role: { enum: ['admin', 'user'], optional: true },
});
type User = typeof User.type
// Automatically infers:
// { id: string; name: string; email: string; age?: number; role?: 'admin' | 'user' }Create a validator that automatically fetches remote $ref schemas:
const validator = await schemaAsync({
$ref: 'https://json-schema.org/draft/2020-12/schema',
});Include validators directly in other schemas for easy composition:
// Define reusable schemas
const Address = schema({
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
zip: { type: 'string' },
},
required: ['street', 'city'],
});
// Include in another schema
const Person = schema({
type: 'object',
properties: {
name: { type: 'string' },
address: Address, // Use validator directly
},
required: ['name', 'address'],
});
type Person = typeof Person.type;
// { name: string; address: { street: string; city: string; zip?: string } }Works in arrays, anyOf, allOf, and any other schema position:
const Team = schema({
type: 'object',
properties: {
name: { type: 'string' },
members: { type: 'array', items: Person }, // Array of Person
headquarters: Address,
},
required: ['name', 'members', 'headquarters'],
});Access the underlying schema via the .schema property:
console.log(Address.schema);
// { type: 'object', properties: { ... }, required: ['street', 'city'] }Automatically coerce values to match schema types:
const Config = schema({
type: 'object',
properties: {
port: { type: 'integer' },
debug: { type: 'boolean' },
timeout: { type: 'number' },
},
}, { coerce: true });
// String values are coerced to match types
const result = Config.validate({ port: '3000', debug: 'true', timeout: '30.5' });
result.value; // { port: 3000, debug: true, timeout: 30.5 }Supported coercions:
- Strings to numbers/integers (
"42"→42) - Strings/numbers to booleans (
"true",1→true) - Strings to null (
"","null"→null) - Single values to arrays (
"item"→["item"])
interface ValidatorOptions {
// Enable format validation (default: false for 2019-09+, true for draft-07 and earlier)
formatAssertion?: boolean;
// Enable content validation for contentMediaType/contentEncoding (default: false)
contentAssertion?: boolean;
// Pre-loaded remote schemas for $ref resolution
remotes?: Record<string, JsonSchema>;
// Use legacy $ref behavior where $ref ignores siblings (default: true)
legacyRef?: boolean;
// Enable type coercion (default: false)
coerce?: boolean | {
string?: boolean;
number?: boolean;
integer?: boolean;
boolean?: boolean;
null?: boolean;
array?: boolean;
};
}MIT