Fast, type-safe TypeScript parsers, validators, types, docs, and test data — generated from JSON Schema. Plus a JSON/YAML linter to keep the schemas themselves in shape.
Warning
mjst is pre-alpha. APIs and generated output will change without notice until 1.0.
mjst is a monorepo of JSON Schema (Draft 2020-12) tooling for TypeScript. At its core are code generators that turn a schema into:
| Output | Description |
|---|---|
| Parsers | Runtime functions that validate and coerce unknown input into typed values |
| Validators | Error-collecting validateX functions plus flat isX boolean type guards |
| Type definitions | TypeScript types matching the schema, with documentation comments (compiled to .d.ts under --build) |
| Test data | fast-check arbitraries for property testing, plus concrete example values |
| Markdown | A configuration-reference table rendered from a schema's properties |
Around the generators sits a wider toolbox:
- API layer —
@amritk/apiturns route contracts into typed handlers, request/response validation, an OpenAPI 3.2 document, and a typed client - Linting —
mjst lintchecks JSON/YAML documents against JSON Schema and custom style rules, with exactline:columnfindings - Adapters — consume schemas authored in TypeBox, Zod, Valibot, Effect, or Apache Avro (
.avsc) as input $refresolution — resolve and inline JSON Schema / OpenAPI$refs, with a default-deny SSRF guard- Runtime validation — fast validation for schemas you don't know ahead of time
- YAML parsing — a tiny, dependency-free YAML parser that keeps exact source positions
The CLI (mjst) is the primary entry point; everything above is also published as a standalone package — see Packages below.
| Package | Description |
|---|---|
@amritk/mjst |
CLI — generates parsers, validators, types, and test data from a schema; lints JSON/YAML (mjst lint); compiles API contracts (mjst compile-api); renders schema docs (mjst markdown) |
@amritk/api |
Contract-first, framework-agnostic API layer — typed routes, request/response validation, OpenAPI 3.2, typed client |
@amritk/lint |
Format-agnostic JSON/YAML style-guide linter — JSON Schema + custom rules, with exact line:column findings |
@amritk/generate-parsers |
Programmatic API for parser + type generation |
@amritk/generate-validators |
Programmatic API for validator generation |
@amritk/runtime-validators |
Runtime JSON Schema validation for schemas not known ahead of time |
@amritk/generate-examples |
Programmatic API for fast-check arbitraries + example data generation |
@amritk/generate-markdown |
Renders a config schema as documentation — a README table, or a multi-page prose reference |
@amritk/adapters |
Convert schemas from external libraries (TypeBox, Zod, Valibot, Effect, Apache Avro) into JSON Schema |
@amritk/asyncapi |
Extract message schemas from AsyncAPI 2.x/3.0 documents for the generators (--input asyncapi) |
@amritk/resolve-refs |
Resolve and inline JSON Schema / OpenAPI $refs, with a default-deny SSRF guard |
@amritk/yaml |
Tiny, dependency-free YAML parser with exact source positions for diagnostics |
@amritk/helpers |
Shared runtime helpers used by generated code |
Most tools in this space pick a single lane — types or validation or docs. mjst generates the whole TypeScript surface from one schema, and it can also consume schemas authored in other libraries as input.
| Types | Validators | Parsers / coercion | Markdown docs | Test data ² | Multi-library input | |
|---|---|---|---|---|---|---|
| mjst | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| json-schema-to-typescript | ✅ | — | — | — | — | — |
| ajv (standalone) | — | ✅ | — | — | — | — |
| quicktype | ✅ | — | 🟡 | — | — | — |
| TypeBox · Zod · Valibot | ✅ | ✅ | ✅ | — | — | n/a ¹ |
✅ first-class · 🟡 partial · — not offered
¹ These libraries are a schema source rather than a competitor — mjst consumes them via @amritk/adapters.
² fast-check arbitraries for property testing plus concrete example values, via @amritk/generate-examples.
mjst's validators are generated TypeScript — straight-line, monomorphic code with no generic dispatch. The exported validateX runs a tiny inlined boolean guard on the happy path and falls back to a separate error-collecting function only when input is actually invalid. On JavaScriptCore that beats every other library measured, the build-time transformer typia included. On V8 it still leads on the object schemas — the shapes an application actually validates — but TypeBox's compiled checker takes the assert-loose moltar shape outright and draws level on assert-strict, so both engines get a row rather than one standing in for the other. The numbers below compare a generated mjst validator against typia, an Ajv-compiled function, a TypeBox-compiled checker, and a hand-written Zod schema on the same data.
Each schema also generates a boolean type-guard isX(input): input is X — a single flat predicate (no error array, no cold-path call) returning the same verdict as validateX. It is the inline-friendly equivalent of TypeBox's compiled check / typia's is, for the common "is this valid?" question where you don't need the error list; validateX remains the rich, error-collecting form.
Steady-state throughput (valid input, higher is better). Each cell is the median of three runs of the whole suite; typia is Bun-only because its checks come from a compile-time transform delivered as a Bun preload:
| schema | runtime | mjst (generated) | typia (transformed) | ajv (compiled) | typebox (compiled) | zod |
|---|---|---|---|---|---|---|
| small (4 fields) | Bun | ~59M ops/s | ~6.7M | ~11M | ~8.9M | ~2.4M |
| small (4 fields) | Node | ~56M ops/s | n/a | ~7.0M | ~6.6M | ~2.2M |
| order (nested + array) | Bun | ~10M ops/s | ~2.6M | ~4.1M | ~3.6M | ~0.50M |
| order (nested + array) | Node | ~8.9M ops/s | n/a | ~2.8M | ~2.9M | ~0.48M |
| assert-loose | Bun | ~190M ops/s | ~170M | ~46M | ~80M | ~3.6M |
| assert-loose | Node | ~90M ops/s | n/a | ~71M | ~138M | ~6.0M |
| assert-strict | Bun | ~171M ops/s | ~58M | ~23M | ~45M | ~1.4M |
| assert-strict | Node | ~37M ops/s | n/a | ~25M | ~37M | ~3.6M |
The assert-loose / assert-strict rows use the same shape as moltar/typescript-runtime-type-benchmarks — they are not that project's numbers, and they are not comparable with its leaderboard. The shape is shared; the harness is not, and the harness is worth an order of magnitude. Every operation on the leaderboard goes through benny (benchmark.js) into a class-property call, around a single frozen module-level fixture whose verdict is discarded. Running the same generated functions under that harness (bun run bench:moltar, one run on Linux x64, Bun 1.4.0 / Node 26.8.1):
| harness | runtime | assert-loose | assert-strict |
|---|---|---|---|
this table (bench/measure.ts) |
Bun | ~190M ops/s | ~171M ops/s |
this table (bench/measure.ts) |
Node | ~90M ops/s | ~37M ops/s |
benny, moltar's Benchmark class |
Bun | ~90M ops/s | ~76M ops/s |
benny, moltar's Benchmark class |
Node | ~80M ops/s | ~34M ops/s |
| no-op control, same harness | Node | ~91M ops/s | ~96M ops/s |
| no-op control, same harness | Bun | ~508M ops/s (±46%) | ~449M ops/s (±48%) |
The no-op row is a "validator" that checks nothing, so it is the fastest number that harness can produce: on Node the assert-loose figure lands within 12% of it, which is a measurement of benny, not of validation — the leaderboard's ceiling, not any library's, and on CI hardware that ceiling sits lower still. The harness also reorders the field: under benny on Node the generated validator leads TypeBox (~80M against ~54M), the reverse of what our own harness reports on the same functions and the same engine. And moltar's fixture is Object.freeze({ … }), which under Bun 1.3 cost every library about 100× on assert-strict (the Bun cell read ~2.4M); Bun 1.4.0 has closed that cliff (details and the frozen-input benchmark: Frozen inputs).
Frozen inputs are their own workload — on older Bun. Enforcing additionalProperties: false means proving no undeclared key is there, and every library does that by enumerating keys. On JavaScriptCore under Bun 1.3, making an object non-extensible (Object.freeze, Object.seal, Object.preventExtensions) disabled the engine's cached own-keys fast path, so every key sweep — Object.keys, for...in, Reflect.ownKeys — dropped to a generic walk. Property reads were unaffected; only strict schemas paid. Bun 1.4.0 no longer shows the cliff: frozen and mutable input run at the same speed for every library. Frozen config objects and frozen fixtures are ordinary inputs, so the bench keeps measuring them (assert-strict (frozen), small (4 fields, frozen)), and the table carries both runtimes on the same machine:
assert-strict, valid input |
Bun 1.4.0 mutable | Bun 1.4.0 frozen | Node 26 mutable | Node 26 frozen | Bun 1.3.11 mutable | Bun 1.3.11 frozen |
|---|---|---|---|---|---|---|
| mjst (generated) | ~171M ops/s | ~166M ops/s | ~37M ops/s | ~35M ops/s | ~82M ops/s | ~1.5M ops/s |
| typia (transformed) | ~58M ops/s | ~89M ops/s | n/a | n/a | ~37M ops/s | ~1.5M ops/s |
| typebox (compiled) | ~45M ops/s | ~46M ops/s | ~37M ops/s | ~35M ops/s | ~27M ops/s | ~1.4M ops/s |
| ajv (compiled) | ~23M ops/s | ~22M ops/s | ~25M ops/s | ~25M ops/s | ~12M ops/s | ~1.2M ops/s |
| zod | ~1.4M ops/s | ~1.4M ops/s | ~3.6M ops/s | ~3.6M ops/s | ~0.91M ops/s | ~0.47M ops/s |
On Bun 1.3 everything converged because everything was paying the same engine slow path; V8 never had the cliff — the Node columns above are flat, frozen or not — and Bun 1.4 has closed it. Frozen inputs has the alternatives that were measured and why the generated code keeps the key count.
Prepare-a-validator cost (one-shot, lower is better):
| runtime | mjst (codegen) | ajv (compile) | typebox (compile) | zod | |
|---|---|---|---|---|---|
| small | Bun | ~0.42 ms | ~12 ms | ~0.09 ms | n/a — authored in code |
| small | Node | ~0.29 ms | ~6.2 ms | ~0.05 ms | n/a — authored in code |
| order | Bun | ~0.66 ms | ~13 ms | ~0.23 ms | n/a — authored in code |
| order | Node | ~0.58 ms | ~6.4 ms | ~0.11 ms | n/a — authored in code |
Measured on Bun 1.4.0 and Node 26.8.1 (Linux x64, a 4-vCPU cloud box — every table in this repo comes from the same machine and runtimes), each cell the median of three runs. Absolutes drift between sittings on that box: the same suite, same commit, read ~60% faster an hour later across every case at once, so the ratios are the durable part. Each library is timed in an isolated process over a pool of distinct inputs, reporting the median of many trials (so the optimiser can't hoist or eliminate the work). Every library agrees on each valid/invalid verdict — parity is asserted before timing — and TypeBox is given uuid/email format checkers so every library does the same work. Reproduce with cd packages/generate-validators && bun run bench.
Parsing replicates both parse modes of the same benchmark — its modes and
its shapes, under this repo's harness rather than the leaderboard's, with the
same caveat as above — over the libraries
with a pure (non-mutating) parse operation. parseSafe asserts the types and
strips undeclared keys (zod's .strip()); parseStrict asserts the types
and rejects undeclared keys (zod's .strict()):
| schema | runtime | mjst (generated) | zod (.parse) |
typebox (Value.Parse) |
|---|---|---|---|---|
| parseSafe — strip extras | ||||
| small (4 fields) | Bun | ~163M ops/s ³ | ~3.3M ops/s | ~1.7M ops/s |
| small (4 fields) | Node | ~88M ops/s ³ | ~4.0M ops/s | ~0.67M ops/s |
| order (nested + array) | Bun | ~7.7M ops/s | ~0.60M ops/s | ~0.26M ops/s |
| order (nested + array) | Node | ~8.2M ops/s | ~0.64M ops/s | ~0.15M ops/s |
| assert (moltar shape) | Bun | ~120M ops/s ³ | ~3.5M ops/s | ~0.87M ops/s |
| assert (moltar shape) | Node | ~66M ops/s | ~5.3M ops/s | ~0.35M ops/s |
| parseStrict — reject extras | ||||
| small (4 fields) | Bun | ~43M ops/s | ~1.9M ops/s | ~2.2M ops/s |
| small (4 fields) | Node | ~55M ops/s | ~2.7M ops/s | ~1.5M ops/s |
| order (nested + array) | Bun | ~13M ops/s | ~0.36M ops/s | ~0.43M ops/s |
| order (nested + array) | Node | ~8.7M ops/s | ~0.55M ops/s | ~0.25M ops/s |
| assert (moltar shape) | Bun | ~44M ops/s | ~1.4M ops/s | ~1.2M ops/s |
| assert (moltar shape) | Node | ~34M ops/s | ~3.5M ops/s | ~0.80M ops/s |
mjst parses in strict mode throughout (throwing on a type mismatch like the others), adding stripUnknown for parseSafe and additionalProperties: false for parseStrict; zod uses .object/.strictObject and TypeBox a Clean+Assert/Assert pipeline. Parity — identical parsed output, and rejection of every wrong-typed (and, in strict mode, extra-keyed) sample — is asserted before timing. ajv (removeAdditional) and typia (assertPrune) are excluded because they strip by mutating the input in place rather than returning a new value, which a reused input pool can't measure fairly. Reproduce with cd packages/generate-parsers && bun run bench (Bun) or bun run bench:node (Node). Unlike the validator table, the generated parser leads every case on both engines. ³ A strip parse of four declared keys builds one small object and nothing else, which is fast enough that the engine's inlining rather than the parser sets the number — read those cells as ratios.
No install required — run it directly with your favourite package runner:
# npm
npx @amritk/mjst --schema ./schema.json --out-dir ./generated
# pnpm
pnpm dlx @amritk/mjst --schema ./schema.json --out-dir ./generated
# yarn
yarn dlx @amritk/mjst --schema ./schema.json --out-dir ./generated
# bun
bunx @amritk/mjst --schema ./schema.json --out-dir ./generatedOr use a config file:
npx @amritk/mjst --config ./mjst.config.jsonTip
If you'd rather add it to a project, install it as a dev dependency:
npm install --save-dev @amritk/mjst # or pnpm add -D / yarn add -D / bun add -dThen use the shorter mjst command in npm scripts or via npx mjst.
See the CLI README for the full flag reference and config file examples.
Using mjst from a coding agent (Cursor, Claude Code, Copilot, …)? mjst is pre-alpha, so it isn't in any model's training data — these docs are written to close that gap:
llms.txt— a curated, link-rich index of every package (llmstxt.org convention), for agents and docs crawlers.llms-full.txt— every package'sAI.mdin one file, to paste straight into a model's context.packages/*/AI.md— per package: the mental model, a minimal runnable example, and the gotchas most likely to trip up an LLM. Each also ships inside the published npm tarball, so an agent can read it fromnode_modules.AGENTS.md— for agents editing this repo (build/test workflow, per-package invariants).
Both llms.txt files are generated from the packages by bun run generate-llms,
so they stay in sync with the source.
- Node.js ≥ 20 (or Bun) to run the CLI
- TypeScript ≥ 5 in your consuming project
Contributing? You'll need Bun — the repo pins 1.4 via packageManager — it's the package manager and bundler for this repo. See CONTRIBUTING.md.
bun install
bun run test # run the test suite
bun run check # lint with biome
bun run build # build all publishable packagesSee .claude/architecture.md for monorepo layout and design notes, and CONTRIBUTING.md for contribution guidelines.