{"_id":"@earendil-works/pi-schema","_rev":"3-eef1ed24b82fdb1d46b0269ede96d4e8","name":"@earendil-works/pi-schema","dist-tags":{"latest":"1.5.0"},"versions":{"0.0.0-stage":{"name":"@earendil-works/pi-schema","version":"0.0.0-stage","_id":"@earendil-works/pi-schema@0.0.0-stage","stub":true,"maintainers":[{"name":"mitsuhiko","email":"armin.ronacher@active-4.com"}],"dist":{"shasum":"272c2134850a0424c46df6cf88c136393c274a44","tarball":"https://registry.npmjs.org/@earendil-works/pi-schema/-/pi-schema-0.0.0-stage.tgz","fileCount":2,"integrity":"sha512-C1mRclxR7v4OFCkJb9XnJCSYfN5ThAXnE8ozWTIdfgU/KPbLPu/kTV+xN1gKrYLNS7IplzwIg51nuDKXkvUaNw==","signatures":[{"sig":"MEYCIQDTqFCDY5W+Vn3noHgMGfKQpoyjY8G97aFa0Ajq87PsQAIhAMNaXiYDJnhDbW0Hfh1MXwOzxkeJzMcIOD9NzprzFqd4","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":347},"_npmUser":{"name":"mitsuhiko","email":"armin.ronacher@active-4.com"},"description":"Temporary package placeholder for staged publishing","directories":{}},"0.0.0":{"name":"@earendil-works/pi-schema","version":"0.0.0","keywords":["json-schema","validation","validator","csp"],"author":{"name":"Earendil Works"},"license":"MIT","_id":"@earendil-works/pi-schema@0.0.0","maintainers":[{"name":"mitsuhiko","email":"armin.ronacher@active-4.com"},{"name":"badlogic","email":"mario@badlogicgames.com"},{"name":"rwachtler","email":"r.wachtler@outlook.com"}],"homepage":"https://github.com/earendil-works/pi#readme","bugs":{"url":"https://github.com/earendil-works/pi/issues"},"dist":{"shasum":"7985b369074676ac150cf46b35c6759351802bf0","tarball":"https://registry.npmjs.org/@earendil-works/pi-schema/-/pi-schema-0.0.0.tgz","fileCount":176,"integrity":"sha512-EYjaf5/fos6kAZIp0ZwDEsSaEkprl1ZemkGuC+zwalfefyOZWlpE8K4/NhEOIzbxwoare+XnV8kzsQ9zuaCkgw==","signatures":[{"sig":"MEUCIGREzi9YXjRzDclEkVdFqKI2wfYewtq2rEn0UweNKDPTAiEA9yYND5l4RVNJKEO+cxYMtBYSERL1E4qigtpI2ApuXtY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIGdK03GCbg4xLJjCzcvWMWr6kckMwF/qkD8hJvGbcuK2AiEAtlAfINSlqBkQHNnL/8gleFX67jk1z93TADlmaEFOBWA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":617374},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=22.19.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","source":"./src/index.ts"},"./formats":{"types":"./dist/formats/index.d.ts","import":"./dist/formats/index.js","source":"./src/formats/index.ts"}},"scripts":{"test":"vitest --run","bench":"node --expose-gc test/bench/all.ts","build":"tsc -p tsconfig.build.json","clean":"node ../../scripts/package-files.mjs clean dist","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"mitsuhiko","email":"armin.ronacher@active-4.com"},"repository":{"url":"git+https://github.com/earendil-works/pi.git","type":"git","directory":"packages/schema"},"_npmVersion":"11.15.0","description":"JSON Schema validation without eval: drafts 2020-12 and 7, compiled into plain closures","directories":{},"sideEffects":false,"_nodeVersion":"25.8.1","_hasShrinkwrap":false,"devDependencies":{"ajv":"8.20.0","zod":"4.6.7","vitest":"4.1.12","typebox":"1.3.27","ajv-formats":"3.0.1","ata-validator":"1.61.3","@oh-my-pi/omptype":"18.13.3","@cfworker/json-schema":"4.1.1"},"_npmOperationalInternal":{"tmp":"tmp/pi-schema_0.0.0_1793614217446_0.5620380425493695","host":"s3://npm-registry-packages-npm-production"}},"1.5.0":{"_id":"@earendil-works/pi-schema@1.5.0","bugs":{"url":"https://github.com/earendil-works/pi/issues"},"dist":{"shasum":"09e65db5dfb6d35edd124b7fc31c7c025d5da160","tarball":"https://registry.npmjs.org/@earendil-works/pi-schema/-/pi-schema-1.5.0.tgz","fileCount":176,"integrity":"sha512-RW0MbT2tR65OY6xMsgB/yVBb7m/RTNwA+9hurSN1vVXfHZRVmVnXbruBPrx/PzbrQnW5yLVlMRWxErGDQtF1Hg==","signatures":[{"sig":"MEQCIEl0JFw2RPfeI0vyHXJuDhjQGS4JiaWTLGyXA58KRcnYAiB4jKwenRQJ86u3uBSknF8xh7ybdvVCE/jccK7Gcv0JYA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCS3hbnXuWUupmGqWcBxWkl+RwM+DX+LIDZlLP35LOycAIgKpDrmZoxseBQ6n2WxHPCMG4EuiETwhIlLNopLWWZk5I="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@earendil-works%2fpi-schema@1.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":617382},"main":"./dist/index.js","name":"@earendil-works/pi-schema","type":"module","_from":"file:/tmp/pi-package-artifacts-SFwuIz/tarballs/earendil-works-pi-schema-1.5.0-456d0c6d3dad.tgz","types":"./dist/index.d.ts","author":{"name":"Earendil Works"},"engines":{"node":">=22.19.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","source":"./src/index.ts"},"./formats":{"types":"./dist/formats/index.d.ts","import":"./dist/formats/index.js","source":"./src/formats/index.ts"}},"license":"MIT","scripts":{"test":"vitest --run","bench":"node --expose-gc test/bench/all.ts","build":"tsc -p tsconfig.build.json","clean":"node ../../scripts/package-files.mjs clean dist","prepublishOnly":"npm run clean && npm run build"},"version":"1.5.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"45e8680e-8117-4609-967c-c812664fb0cf"}},"homepage":"https://github.com/earendil-works/pi#readme","keywords":["json-schema","validation","validator","csp"],"_resolved":"/tmp/pi-package-artifacts-SFwuIz/tarballs/earendil-works-pi-schema-1.5.0-456d0c6d3dad.tgz","_integrity":"sha512-RW0MbT2tR65OY6xMsgB/yVBb7m/RTNwA+9hurSN1vVXfHZRVmVnXbruBPrx/PzbrQnW5yLVlMRWxErGDQtF1Hg==","repository":{"url":"git+https://github.com/earendil-works/pi.git","type":"git","directory":"packages/schema"},"_npmVersion":"11.16.0","description":"JSON Schema validation without eval: drafts 2020-12 and 7, compiled into plain closures","directories":{},"maintainers":[{"name":"mitsuhiko","email":"armin.ronacher@active-4.com"},{"name":"badlogic","email":"mario@badlogicgames.com"},{"name":"rwachtler","email":"r.wachtler@outlook.com"}],"sideEffects":false,"_nodeVersion":"22.23.4","_hasShrinkwrap":false,"devDependencies":{"ajv":"8.20.0","zod":"4.6.7","vitest":"4.1.12","typebox":"1.3.27","ajv-formats":"3.0.1","ata-validator":"1.61.3","@oh-my-pi/omptype":"18.13.3","@cfworker/json-schema":"4.1.1"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pi-schema_1.5.0_1793617094825_0.39539242145699427"}}},"time":{"created":"2026-11-02T10:07:38.097Z","modified":"2026-11-02T10:58:15.263Z","0.0.0-stage":"2026-11-02T10:07:38.097Z","0.0.0":"2026-11-02T10:10:17.538Z","1.5.0":"2026-11-02T10:58:14.914Z"},"bugs":{"url":"https://github.com/earendil-works/pi/issues"},"author":{"name":"Earendil Works"},"license":"MIT","homepage":"https://github.com/earendil-works/pi#readme","keywords":["json-schema","validation","validator","csp"],"repository":{"url":"git+https://github.com/earendil-works/pi.git","type":"git","directory":"packages/schema"},"description":"JSON Schema validation without eval: drafts 2020-12 and 7, compiled into plain closures","maintainers":[{"name":"mitsuhiko","email":"armin.ronacher@active-4.com"},{"name":"badlogic","email":"mario@badlogicgames.com"},{"name":"rwachtler","email":"r.wachtler@outlook.com"}],"readme":"# @earendil-works/pi-schema\n\nJSON Schema builders and validation without `eval`. A schema compiles once into plain closures, so it checks values\nat close to the speed of validators that generate code, also under a strict Content Security Policy, and needs no\ndependencies.\n\n- Drafts 2020-12 and 7. Passes 2,222 of the 2,223 core tests of the official\n  [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).\n- Builders return plain JSON Schema typed with the values it accepts; a compiled check narrows to that type.\n- Errors as JSON Pointer paths. A missing or unknown field is reported at the field.\n- Never modifies a schema; frozen schemas work.\n- 8.5 KB minified and gzipped. Format checks are an optional import.\n\n## Installation\n\n```bash\nnpm install @earendil-works/pi-schema\n```\n\n## Usage\n\n```typescript\nimport { compile, int, obj, oneOf, opt, type Static, str } from \"@earendil-works/pi-schema\";\n\nconst Read = obj({\n\tpath: str({ description: \"Path to the file\", minLength: 1 }),\n\toffset: opt(int({ minimum: 1 })),\n\tmode: oneOf([\"text\", \"binary\"]),\n});\ntype Read = Static<typeof Read>; // { path: string; mode: \"text\" | \"binary\"; offset?: number }\n\nconst read = compile(Read);\nif (read.check(value)) value.path; // value: Read\n\nread.errors({ offset: 0, extra: true });\n// [\n//   { path: \"/path\", keyword: \"required\", message: \"missing\" },\n//   { path: \"/mode\", keyword: \"required\", message: \"missing\" },\n//   { path: \"/offset\", keyword: \"minimum\", message: \"expected >= 1\" },\n//   { path: \"/extra\", keyword: \"additionalProperties\", message: \"unknown field\" },\n// ]\n```\n\n`Read` is plain JSON Schema (`{ type: \"object\", properties: {...}, required: [\"path\", \"mode\"], additionalProperties:\nfalse }`), ready for a tool definition or `JSON.stringify`. `compile` takes any JSON Schema, built with the builders\nor not.\n\n`check(value)` is the fast path: a boolean, no allocations for most schemas, and a plain function you can pass\naround. `errors(value)` explains a failure and returns `[]` for a valid value. It is built on the first failure and\nkept, so a schema whose values always pass never pays for it.\n\n### Builders\n\n| builder | schema | type |\n|---|---|---|\n| `str({ minLength, maxLength, pattern, format })` | `{ type: \"string\", ... }` | `string` |\n| `num({ minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf })`, `int(...)` | `{ type: \"number\" }`, `{ type: \"integer\" }` | `number` |\n| `bool()`, `nul()`, `unknown()` | `{ type: \"boolean\" }`, `{ type: \"null\" }`, `{}` | `boolean`, `null`, `unknown` |\n| `lit(value)` | `{ const: value }` | the literal |\n| `oneOf([\"a\", \"b\"])` | `{ enum: [...] }` | `\"a\" \\| \"b\"` |\n| `obj({ a: str(), b: opt(num()) }, { open })` | closed object; `open: true` allows other properties | `{ a: string; b?: number }` |\n| `arr(items, { minItems, maxItems, uniqueItems })` | `{ type: \"array\", items }` | `T[]` |\n| `tuple([str(), num()])` | `prefixItems`, no more items | `[string, number]` |\n| `map(value)` | `{ type: \"object\", additionalProperties: value }` | `Record<string, T>` |\n| `union([...])` | `{ anyOf: [...] }` | the union |\n| `nullable(schema)` | `{ anyOf: [schema, { type: \"null\" }] }` | `T \\| null` |\n| `bytes({ minLength, maxLength })` | `{ type: \"bytes\", ... }` | `Uint8Array` |\n\nEvery builder also takes `title`, `description`, `examples`, `deprecated` and `default`. A `default` is an annotation;\nvalidation never inserts it.\n\nA union of objects tagged by a `lit()` property checks and reports by the tag: only the branch the tag selects runs.\n\n```typescript\nconst Block = union([obj({ kind: lit(\"text\"), text: str() }), obj({ kind: lit(\"image\"), width: int({ minimum: 0 }) })]);\ncompile(Block).errors({ kind: \"video\" });\n// [{ path: \"/kind\", keyword: \"anyOf\", message: 'expected one of [\"text\",\"image\"]' }]\n```\n\n`Static` also types schemas built with TypeBox 1.x, read from TypeBox's type shapes (pi-schema does not import\nTypeBox): `String`, `Number`, `Integer`, `Boolean`, `Null`, `Literal`, `Array`, `Object` (with `Type.Optional` and\n`Type.Readonly` properties), `Union`, `Record`, `Unsafe`, `Unknown` and `Any` give TypeBox's own static type, so\n`Static<typeof Type.Object({ path: Type.String() })>` is `{ path: string }`. Other TypeBox kinds (`Enum`, `Tuple`,\n`Intersect`, `Cyclic`, ...) give `unknown` where they stand. Other JSON Schema objects give `unknown`.\n\n### Options\n\n```typescript\nimport { formats } from \"@earendil-works/pi-schema/formats\";\n\nconst validator = compile(schema, {\n\tdraft: \"7\", // \"2020-12\" (default) or \"7\"\n\tformats, // check `format`; without it `format` is an annotation, as 2020-12 specifies\n\tschemas: { \"https://example.com/point.json\": point }, // documents for $refs to other documents, by URI\n\tunresolvedRefs: \"reject\", // a $ref that names nothing rejects values at its place; default \"throw\"\n\tcoerce: true, // adds coerce() and parse(), see Coercion\n});\n```\n\n`formats` has every format 2020-12 defines (`date-time`, `email`, `uri`, `uuid`, `ipv6`, ...), plus `url` and\n`json-pointer-uri-fragment`, which TypeBox and ajv-formats also check. Pass your own map to check fewer formats, or\ncustom ones.\n\nA `$ref` that names nothing (a missing definition, a document not in `schemas`) throws when the schema compiles. With\n`unresolvedRefs: \"reject\"` it compiles as `false` instead: only values at that place fail, with the error\n`unresolved reference <ref>`, and the rest of the schema works. Use it for schemas from other systems, such as MCP\nservers, whose references may dangle.\n\n### TypeBox schemas\n\nTypeBox 1.x schemas are JSON Schema, so they compile as they are:\n\n```typescript\nimport Type from \"typebox\";\n\nconst user = compile(Type.Object({ name: Type.String(), tags: Type.Array(Type.String()) }));\n```\n\nTypeBox's JavaScript-only types (`BigInt`, `Undefined`, `Function`, ...), refinements and codecs are not JSON Schema\nand are not supported.\n\n### Coercion\n\n`compile(schema, { coerce: true })` adds `coerce()` and `parse()`, for loosely typed input such as a model's tool call\narguments. Without the option the validator has neither, and its `check` pays nothing for them: the coercion rules are\nbuilt on the first `coerce()` or `parse()`.\n\n```typescript\nconst read = compile(Read, { coerce: true });\n\nread.coerce({ path: \"a.txt\", offset: \"10\", mode: \"text\" }); // { path: \"a.txt\", offset: 10, mode: \"text\" }\n\nconst result = read.parse({ path: \"a.txt\", offset: \"ten\", mode: \"text\" });\nif (result.ok) result.value.offset; // number\nelse result.errors; // [{ path: \"/offset\", keyword: \"type\", message: \"expected integer, got string\" }]\n```\n\n`coerce(value)` returns the coerced value. It copies only the paths it changes, returns `value` itself when nothing\nchanges, and never modifies `value`. A value no rule applies to stays as it is. `parse(value)` coerces, then checks:\n`{ ok: true, value }` with the coerced value, or `{ ok: false, errors }` with the errors of the coerced value.\n\nThree passes run in order:\n\n1. **Optional nulls.** A `null` in an optional property whose schema rejects `null` is dropped, as if omitted.\n2. **Conversions**, ported from TypeBox 1.3.27's `Value.Convert` and keyed on JSON Schema keywords (the first that\n   applies to a schema): `$ref` follows the reference; `const` and `enum` take the member the value converts to;\n   `anyOf`/`oneOf` keep a value one branch accepts, else take the first branch's conversion the whole schema\n   accepts; `allOf` converts with each member in turn; then by `type`:\n   - `string`: numbers and booleans to their text, `null` to `\"null\"`.\n   - `number`: numeric strings (`\"\"` is 0), `\"true\"`/`\"false\"`, booleans to 1/0, `null` to 0.\n   - `integer`: as `number`, then truncated (`\"3.5\"` and `3.5` become 3).\n   - `boolean`: `\"true\"`/`\"false\"` (any case), `\"1\"`/`\"0\"`, 1/0, `null` to `false`.\n   - `null`: `\"null\"`, `\"undefined\"`, `\"\"`, `\"0\"`, 0 and `false`.\n   - `array`: a single value becomes a one-item array, then each item converts; a tuple (`prefixItems` or an `items`\n     array) converts its items and never wraps.\n   - `object`: each property converts by `properties`, `patternProperties` or `additionalProperties`.\n3. **Primitive coercion by type**, as Ajv's `coerceTypes`, for what pass 2 leaves (type arrays such as\n   `[\"integer\", \"null\"]`): numbers from numeric strings and booleans, booleans from `\"true\"`/`\"false\"` and 1/0,\n   strings from numbers and booleans, `null` from `\"\"`, 0 and `false`, and `null` to `\"\"`, 0 or `false`. A value that\n   already has one of a type array's types stays.\n\nWhere passes 2 and 3 disagree, pass 2 wins: `null` for a required string becomes `\"null\"`, not `\"\"`. A union's\nbranches convert in order and only until one is accepted; a union node that also declares properties converts them into\neach branch's candidate before checking it. A recursive schema (a tree through `$ref`) that meets a value it cannot\nconvert leaves the value as it is, so `check` reports it, and results through a `$ref` are reused within one call, so a\nrecursive union does not convert (or explain) the same child once per branch.\n\nPass 1 drops optional nulls inside the values of `properties`, `patternProperties` and `additionalProperties` alike; a\npattern or additional key itself is never dropped. Items past a draft-7 `items` array (`additionalItems`) are not\ncoerced by any pass.\n\n### Values\n\n- A property whose value is `undefined` counts as absent, as in TypeBox.\n- `{ type: \"bytes\", minLength, maxLength }` matches a `Uint8Array` of that length, an extension for values that never\n  pass through JSON.\n\n## Supported keywords\n\n- Every validation keyword of drafts 2020-12 and 7, including draft 7's `dependencies` and `items` arrays with\n  `additionalItems`.\n- `$id`, `$anchor`, `$ref` (local, by anchor, relative and remote), `$dynamicRef` and `$dynamicAnchor`.\n- `unevaluatedProperties` and `unevaluatedItems`.\n- `pattern` and `patternProperties` are ECMAScript regular expressions with the `u` flag.\n- Not supported: meta-schemas that switch vocabularies on or off (`$vocabulary`), `contentMediaType` and\n  `contentEncoding` assertions (optional in the spec), and draft 2019-09's `$recursiveRef`.\n\n## Benchmarks\n\nNode 24.4.1 on one core of an i9-13900. The other libraries:\n\n- ata-validator 1.61.3: its generated code, and its eval-free interpreter chosen with `engine: \"interpreter\"`;\n- Ajv 8.20.0, with ajv-formats 3.0.1 for the format tests;\n- Zod 4.6.7: native Zod schemas for speed, setup and memory, and `z.fromJSONSchema` for the test suite;\n- TypeBox 1.3.27;\n- @cfworker/json-schema 4.1.1;\n- @oh-my-pi/omptype 18.13.3, through `fromJsonSchema`.\n\n| test suite (tests passed) | pi-schema | ata | ata interpreter | Ajv | Zod | TypeBox | cfworker | omptype |\n|---|---:|---:|---:|---:|---:|---:|---:|---:|\n| draft 2020-12 core (1,298) | 1,297 | 1,298 | 1,298 | 1,236 | 693 | 1,276 | 1,249 | 863 |\n| draft 7 core (925) | 925 | 925 | 925 | 917 | 632 | 924 | 918 | 659 |\n| draft 2020-12 format (895), with format checks | 895 | 814 | 814 | 750 | 412 | 895 | 740 | 412 |\n\n| check, valid value | pi-schema | ata | ata interpreter | Ajv | Zod | TypeBox | cfworker | omptype |\n|---|---:|---:|---:|---:|---:|---:|---:|---:|\n| nested object with tagged unions | 410 ns | 180 ns | 836 ns | 627 ns | 1.3 µs | 475 ns | 43 µs | 730 ns |\n| union of 5 messages | 86 ns | 37 ns | 212 ns | 70 ns | 297 ns | 95 ns | 11 µs | 96 ns |\n| tool schema (read) | 31 ns | 10 ns | 41 ns | 13 ns | 41 ns | 9 ns | 2.0 µs | 9 ns |\n| MCP tool schema with `$ref` | 161 ns | 64 ns | 314 ns | 136 ns | 927 ns | 87 ns | 15 µs | 1.9 µs |\n| recursive `$ref` schema | 755 ns | 307 ns | 1.9 µs | 378 ns | 2.7 µs | 1.2 µs | 41 µs | 7.5 µs |\n\n| same, without eval (strict CSP) | pi-schema | ata | ata interpreter | Ajv | Zod | TypeBox | cfworker | omptype |\n|---|---:|---:|---:|---:|---:|---:|---:|---:|\n| nested object with tagged unions | 411 ns | 1.6 µs | 844 ns | fails | 3.4 µs | 44 µs | 44 µs | 2.7 µs |\n| union of 5 messages | 87 ns | 396 ns | 209 ns | fails | 652 ns | 10 µs | 11 µs | 501 ns |\n| MCP tool schema with `$ref` | 164 ns | 484 ns | 326 ns | fails | 2.0 µs | 39 µs | 13 µs | 2.2 µs |\n| recursive `$ref` schema | 746 ns | 2.8 µs | 1.9 µs | fails | 7.3 µs | 130 µs | 42 µs | 9.0 µs |\n\n| cost | pi-schema | ata | ata interpreter | Ajv | Zod | TypeBox | cfworker | omptype |\n|---|---:|---:|---:|---:|---:|---:|---:|---:|\n| compile a new schema | 20 µs | 4.9 µs | 1.3 µs | 1.4 ms | 151 µs | 210 µs | 22 µs | 28 µs |\n| its first check | 0.8 µs | 335 µs | 87 µs | 195 µs | 466 µs | 41 µs | 7.1 µs | 109 µs |\n| memory, 3,200 distinct schemas | 7.1 MB | 36.0 MB | 51.3 MB | 59.7 MB | 170.8 MB | 54.7 MB | 7.6 MB | 19.9 MB |\n| bundle, minified + gzip | 8.5 KB | 134.0 KB | 134.0 KB | 39.7 KB | 24.4 KB | 35.2 KB | 5.8 KB | 38.8 KB |\n\nWhere code generation is unavailable:\n\n- **ata** answers through its interpreted engine without being told. That automatic fallback measured slower than the\n  engine chosen with `engine: \"interpreter\"`.\n- **Ajv** cannot compile at all: `compile()` throws `EvalError`.\n- **Zod** notices and parses objects without its generated code.\n- **TypeBox** falls back to its interpreter.\n- **omptype** throws `EvalError` once per schema, then validates through its interpreter.\n\nata, Zod and omptype compile on a schema's first check, so their compile cost shows there. ata's bundle is for the\nbrowser without code splitting; with splitting, its first load is about 53 KB plus a 76 KB generator chunk loaded\nlater.\n\nZod is not a JSON Schema validator:\n\n- **Speed, setup and memory:** Zod gets the same schemas written as native Zod schemas (`z.strictObject`,\n  `z.discriminatedUnion`, `z.lazy`), with `.describe()` for the corpus descriptions, and `safeParse` as the check.\n  Its memory includes building the schemas, which the other libraries receive as JSON. Without the descriptions it\n  is 131 MB.\n- **Test suite:** through `z.fromJSONSchema`, which is semi-experimental. It refuses `unevaluatedProperties`,\n  `unevaluatedItems`, `dependentSchemas`, `dependentRequired`, `if`/`then`/`else`, `not`, and every `$ref` that is\n  not local. A refused schema fails all its tests: 405 of the 1,298 draft 2020-12 core tests and 107 of the 925 draft\n  7 core tests.\n- **Bundle:** measured with Zod's documented `import * as z from \"zod\"`. A named `import { z }` keeps every locale\n  (about 90 KB gzipped), and `z.fromJSONSchema` brings it to 40.0 KB; `zod/mini` is about 6 KB.\n\nFlat objects of simple types check in 13–80 ns, against 7–15 ns for TypeBox. Without code generation, reading a\nproperty whose name comes from a list costs a few nanoseconds.\n\n### Reproduce\n\n`npm run bench` in `packages/schema` takes about 12 minutes. It runs every library through its fastest boolean path\n(ata's `isValidObject()`) and keeps the object each returns. ata runs with `ATA_NO_NATIVE=1`: its optional native\naddon only serves its buffer APIs. The scripts are in `test/bench/`.\n\n- **Conformance:** every library on the vendored test suite, with the remote documents and meta-schemas the tests\n  reference (Zod through `z.fromJSONSchema`, which takes no remote documents). ata reads the draft only from `$schema`, so draft 7 schemas without one get it declared. ata's defaults\n  (`useDefaults`, `assertFormat`) are turned off to match the other libraries; formats are checked only in the\n  format tests, by each library's own checks.\n- **Speed:** realistic schemas, in ns per check after warm-up. Each result is the median of 15 rounds, with the\n  libraries' order rotated in each round.\n- **Speed without eval:** the same, run with `--disallow-code-generation-from-strings`.\n- **Setup:** compiling 1,000 schemas a library has not seen, and their first checks.\n- **Memory:** 3,200 distinct tool-like schemas, compiled and each checked 3 times, each library in its own process.\n- **Size:** esbuild browser bundles of what a validator needs, minified and gzipped.\n\n### Code generated at build time\n\nAjv (`ajv/dist/standalone`) and ata (`ata build`, `ata compile`, `toStandaloneModule()`) can also generate their\nvalidation code when the app is built. The app then imports plain modules and evaluates nothing at run time, so those\nchecks run at code-generation speed under a strict CSP. This is not in the benchmark, because it is a different\ndeal:\n\n- **A build step per schema.** Every schema must be known when the app is built. Schemas that arrive at run time\n  (MCP servers, extensions, user input) cannot be validated this way.\n- **Code per schema.** In a separate measurement on the same 3,200 schemas:\n  - Ajv: 1.8 ms to build and 8.9 KB of JavaScript per schema;\n  - ata: 1.1 ms and 13 KB per schema, or 0.3–0.8 KB gzipped when only its `isValid` export is bundled;\n  - importing all 3,200 generated validators took 40 MB of heap (Ajv, one module) and 68 MB (ata, one module per\n    schema).\n\npi-schema compiles at run time in 25 µs per schema, generates no code, and covers schemas known at build time and\nschemas that arrive later.\n\n## Development\n\n`npm test` runs the official test suite (drafts 2020-12 and 7, including the optional and format tests, vendored in\n`test/fixtures/`) and the package's own tests.\n\nThe format checks are adapted from TypeBox 1.3.27 (MIT); its license is in `LICENSES/`.\n","readmeFilename":"README.md"}