@ytrynot/schvalid
v0.3.13
Published
JSON Schema 2020-12 validator with compiled standalone JS function using DNA bytecodes. OpenAPI 3.1 discriminator support. Close to AJV's speed.
Downloads
1,454
Maintainers
Readme
@ytrynot/schvalid
Looking for testers! This package is actively seeking early users and feedback. If you try it out, please share your experience — issues, suggestions, or ideas are all welcome.
npm: https://www.npmjs.com/package/@ytrynot/schvalid · GitHub: https://github.com/linqFR/ytn/tree/main/packages/schvalid
JSON Schema 2020-12 validation with compiled standalone JS functions.
Important: This package only supports and validates JSON Schema 2020-12 with internal references. External
$ref(HTTP URIs, URNs, or external files) are not supported.
Table of Contents
Overview
@ytrynot/schvalid provides JSON Schema to DNA bytecode conversion and validation using the high-performance DNA engine from @ytrynot/dna. It serves as the primary interface for JSON Schema validation in the ytrynot ecosystem.
Installation
npm install @ytrynot/schvalidAgent Skills
Install the ytn agent skill so your AI coding agent knows how to use this package:
npx skills add linqFR/ytnLimitations
External URIs: This package does not currently handle external JSON Schema references ($ref pointing to external files or HTTP URIs). Only internal references within the same schema document are supported.
Comparison with AJV
@ytrynot/schvalid covers all core JSON Schema 2020-12 keywords with full parity — types, object/array constraints, const/enum, allOf/anyOf/oneOf, if/then/else, not, patternProperties, dependentRequired/Schemas, internal $ref, $id, $defs, discriminator. It does not aim to replace AJV in all use cases. Key differences:
- schvalid adds: DNA bytecode intermediate representation (IR),
parseFasthybrid mode, three-mode compilation API, parser output construction, standalone JS viatoJS(), faster compilation and validation than AJV. - schvalid lacks: external $ref, custom formats, user-defined keywords, async validation, $data, type coercion, default injection, removeAdditional, vocabularies, schema registry, multi-draft support.
Full feature-by-feature comparison: docs/ajv-comparison.md.
Usage
Converting JSON Schema to DNA
import { jschemaToDna } from "@ytrynot/schvalid";
const schema = {
type: "object",
properties: {
name: { type: "string", minLength: 3 },
age: { type: "number", minimum: 0 },
},
};
const dna = jschemaToDna(schema);
// Returns DNA bytecode arrayCompile Once, Validate Many
For performance-critical scenarios, use the schvalid() builder API to compile a schema once and reuse the validation function:
import { schvalid } from "@ytrynot/schvalid";
const schema = {
type: "object",
properties: {
name: { type: "string", minLength: 3 },
age: { type: "number", minimum: 0 },
},
};
// Compile once
const compiler = schvalid("validation");
const validate = compiler.compile(schema);
// Validate many times efficiently
validate({ name: "John", age: 30 }); // true
validate({ name: "Jo", age: -1 }); // falseThe schvalid() function accepts four modes:
- "validation": Returns a boolean validator function (fail-fast)
- "parser": Returns a parser function with error collection
- "fast": Returns a hybrid parser — validates first, only re-runs the full parser on failure (see trade-offs below)
- "all": Returns an object with
validate,parse, andparseFastfunctions (compiled once, shared instances)
import { schvalid } from "@ytrynot/schvalid";
// Get validator, parser, and the fast hybrid parser
const compiler = schvalid("all");
const { validate, parse, parseFast } = compiler.compile(schema);
validate(data); // boolean
parse(data); // { success: true, data: ... } | { success: false, errors: [...] }
parseFast(data); // same shape as parse(), but data===input on the happy path (no fresh copy)Fast Hybrid Parsing
schvalid("fast") (and parseFast from schvalid("all")) provides a hybrid parser that
validates first (cheap, fail-fast) and only re-runs the full parser if validation fails:
import { schvalid } from "@ytrynot/schvalid";
const parseFast = schvalid("fast").compile(schema);
const result = parseFast({ name: "John", age: 30 });
// { success: true, data: { name: "John", age: 30 } }Trade-off: on success, parseFast's data is the same reference as the input
(data === input) — no fresh copy is built, unlike schvalid("parser")'s parse(), which
always returns a newly constructed output object. Both agree on validity (constraints like
additionalProperties: false are checked identically), so there's no discrepancy in
pass/fail decisions — only in whether data is a fresh object or the original reference.
Use parseFast for validation-heavy workloads where a fresh, isolated data object isn't
required on the happy path. Use the regular parser() when downstream code needs its own
copy of the validated data.
// Get validate + parse + parseFast in one compile pass (single validate/parse compilation,
// shared between parse() and parseFast() — see @ytrynot/schvalid AGENTS.md for the invariant)
const { validate, parse, parseFast } = schvalid("all").compile(schema);Discriminator Support
DNA Schema supports the OpenAPI 3.1 discriminator keyword for optimized validation of polymorphic schemas:
import { schvalid } from "@ytrynot/schvalid";
const schema = {
type: "object",
discriminator: {
propertyName: "type",
},
required: ["type", "name"],
oneOf: [
{
type: "object",
properties: {
type: { const: "cat" },
name: { type: "string" },
meows: { type: "boolean" },
},
},
{
type: "object",
properties: {
type: { const: "dog" },
name: { type: "string" },
barks: { type: "boolean" },
},
},
],
};
const { validate, parse } = schvalid("all").compile(schema);
validate({ type: "cat", name: "Whiskers", meows: true }); // true
validate({ type: "bird", name: "Tweety" }); // false
const result = parse({ type: "cat", name: "Whiskers", meows: true });
// Returns: { success: true, data: { type: "cat", name: "Whiskers", meows: true } }The discriminator is optimized with a switch statement in the generated JavaScript code for efficient dispatching to the correct sub-schema based on the discriminator property value.
additionalProperties (and especially additionalProperties: false) defined on the root schema is inherited by each oneOf branch so that unknown properties are rejected while the discriminator property itself is still allowed.
Performance
Benchmark Results (vs AJV 2020 — run npm run bench to reproduce on your machine):
- Compilation: faster than AJV Minimal (~4x on the reference schema).
- Validation (valid data): faster than AJV Minimal.
parseFast(valid data, no error): faster than AJV Minimal. Returns{ success: true, data }(same reference as input — no copy). On invalid data it is slower than AJV AllErrors because it runs the fast validator first, then falls back to the full parser to collect detailed errors — a deliberate trade-off for the common case where most inputs are valid.- Parser mode: not directly comparable to AJV — AJV is validation-only (returns boolean), while
parserconstructs a freshObject.create(null)output object with validated properties, like Zod'sparse(). The generated function is ~30% smaller than AJV's, but the benchmark is slower because it does strictly more work (allocation + copy + reconstruction). This is a different contract, not a speed regression.
Benchmark results vary across machines and runs. Run npm run bench yourself to get numbers for your environment.
Which mode should I use?
- Use
validationfor plain fail-fast boolean checks. - Use
parseFastwhen you need detailed errors on failure but don’t need a fresh output object on success.parseFastruns the cheap fail-fast validator first; if the input is invalid, it falls back to the full parser to collect all errors. It is the fastest rich-error path and the one most users want. - Use
parseronly when you explicitly need a fresh,Object.create(null)output object with the original unknown properties preserved (the same contract as Zodparse()). It is slower than all above, because it is aparse+transformoperation, not just a validator: it allocates anObject.create(null)object, copies the input, rebuilds arrays, and returns{ success, data }. That reconstruction is why it is slower than AJV on the reference benchmark.
Development
Build
npm run buildTesting
# Run JSON Schema test suite plus discriminator and edge-cases tests
npm test
# Run all correctness tests
npm run test:full
# Run all benchmarks (standalone tsx, not vitest; `bench` is an alias of `perf`)
npm run bench
# or
npm run perfTest Coverage of JSON validation Suite: 1243 passing per mode, 44 skipped.
- The 44 skipped tests are from the JSON Schema Test Suite and involve external references (
$refto HTTP URIs, URNs, or external files), which are explicitly out of scope for DNA Schema (only internal references are supported).
The full test suite includes:
- JSON Schema Test Suite: Comprehensive validation against official JSON Schema 2020-12 test cases. For more information, read JSON Schema Validation Suite. Skipped:
refRemote.json,dynamicRef.json,content.json,vocabulary.json. - Discriminator Tests: Full coverage of OpenAPI 3.1 discriminator keyword with validator and parser modes.
- Performance Benchmarks: Comparative benchmarks against AJV for compilation and validation speed.
Peer Dependencies
zod: ^4.4.3
Dependencies
@ytrynot/dna: * (workspace dependency)
License
MIT
Author
linqFR
