@exodus/schemasafe
@exodus/schemasafe compiles JSON Schema into fast JavaScript validators or combined JSON parsers and validators. It supports drafts 04, 06, 07, 2019-09, and 2020-12, rejects schemas with unknown or incoherent keywords by default, can enforce stronger coverage and complexity rules, and can emit self-contained validator modules for content-security-policy environments. It has no runtime dependencies and includes optional error locations, external references, custom formats, default insertion, extra-property removal, and an experimental schema linter.
A sharp choice for small, auditable, fail-closed JSON Schema validation, especially when parser mode prevents unvalidated objects from entering the program. Pick Ajv for ecosystem breadth or Zod for TypeScript-first models, and plan around schemasafe's slow release cadence and incomplete typings.
Use it if
- You validate security-sensitive JSON and want compilation to fail when schema keywords are unknown, unused, unreachable, or incoherent
- You want a parser that never exposes a successfully parsed but unvalidated JSON object to application code
- You need zero runtime dependencies and self-contained generated validators that can be produced during a build for strict CSP
- You want strong mode to require schema declarations, validation coverage, string checks, and basic regular-expression or uniqueItems complexity bounds
- You need the broadest JSON Schema ecosystem, plug-ins, standalone documentation, or current TypeScript inference: Ajv is the common default and schemasafe's own declaration file says its typings are experimental and incomplete
- Your schemas intentionally use annotation or extension keywords the compiler does not understand: schemasafe fails closed unless you relax allowUnusedKeywords, which strong mode forbids
- You cannot run generated code and do not have a build step: runtime compilation creates JavaScript functions, while strict CSP requires calling toModule() ahead of time and bundling the result
- You need draft-03 or vendor-specific behavior beyond its documented strict subset of OpenAPI discriminator: supported targets start at draft-04
- You want high maintenance velocity: npm 1.3.0 was published in August 2023, the repository's latest push was in May 2025, and 19 issues and pull requests are open
Setup reality
npm install @exodus/schemasafe gives a CommonJS package with no dependencies and an included declaration file. There is no peer setup, but the important choice is validator() versus parser(). validator() compiles in default mode and accepts an already parsed value. parser() accepts raw JSON text and uses strong mode by default, which can reject schemas that other validators accept: it requires $schema, requires every object property and array item path to be validated, requires string checks using format, pattern, or contentSchema, and enables complexity checks. Start by compiling schemas during application startup or build, not per request. Schema compilation errors always throw synchronously; invalid data returns false or a parser result with valid: false. Error details are off by default, and allErrors does nothing useful unless includeErrors is also true. Runtime compilation depends on generated JavaScript execution, so a strict Content-Security-Policy should use toModule() in a trusted build and import the output. Treat schemas as trusted configuration even though code generation JSON-escapes schema content: the security document says untrusted schemas can still cause denial of service. useDefaults and removeAdditional mutate validated output and deliberately refuse ambiguous schemas. Formats are asserted by default, differing from the newer JSON Schema specification's annotation default unless mode: 'spec' is selected. The TypeScript declaration openly calls itself experimental and incomplete, so do not expect a schema to infer a precise output type. Add separate input-size limits; strong mode checks for missing bounds around certain expensive patterns but does not judge whether a supplied maximum is sensible.
Patterns
Compile and run a basic validatorcompile-validator
const { validator } = require('@exodus/schemasafe');
const validate = validator({
type: 'object',
required: ['name'],
properties: { name: { type: 'string' } },
additionalProperties: false,
});
console.log(validate({ name: 'Ada' })); // true
console.log(validate({})); // falsevalidator() uses default mode, not strong mode, and accepts a value that has already been parsed or constructed.
Parse raw JSON only when it validatesparse-and-validate
const { parser } = require('@exodus/schemasafe');
const parseUser = parser({
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
},
additionalProperties: false,
});
const result = parseUser('{"name":"Ada"}');
if (result.valid) console.log(result.value);parser() runs strong mode by default and returns no value when either JSON parsing or schema validation fails.
Compile an object validator in strong modeenable-strong-mode
const validate = validator({
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
required: ['email'],
properties: {
email: { type: 'string', format: 'email', maxLength: 254 },
},
additionalProperties: false,
}, { mode: 'strong' });Strong mode refuses uncovered properties and unchecked strings, so a permissive schema may fail during compilation before any data is tested.
Inspect the first validation errorinclude-validation-errors
const validate = validator(schema, { includeErrors: true });
if (!validate({ hello: 100 })) {
console.log(validate.errors);
// [{ keywordLocation: '#/properties/hello/type', instanceLocation: '#/hello' }]
}Errors live on the compiled function and are overwritten by later calls; copy them immediately in concurrent or shared-validator code.
Return more than the first errorcollect-all-errors
const validate = validator(schema, {
includeErrors: true,
allErrors: true,
});
validate(candidate);
for (const error of validate.errors ?? []) {
console.log(error.instanceLocation, error.keywordLocation);
}allErrors requires includeErrors. Some expensive checks are skipped after the same property has already failed, limiting error collection as a denial-of-service precaution.
Validate a custom string formatdefine-custom-format
const validate = validator(
{ type: 'string', format: 'hex-color' },
{ formats: { 'hex-color': /^#[0-9a-f]{6}$/i } },
);
console.log(validate('#ff8800')); // trueCustom format functions and regular expressions are trusted code, not schema data; strong mode disables weak RegExp-object formats, so use a function there if needed.
Resolve an external $refresolve-external-schema
const address = {
$id: 'https://example.test/address',
type: 'object',
properties: { city: { type: 'string' } },
};
const validate = validator(
{ $ref: 'https://example.test/address' },
{ schemas: [address] },
);External schemas can be an array with top-level $id values, a Map, or an object keyed by the referenced name.
Insert schema defaults during validationapply-default-values
const validate = validator({
type: 'object',
properties: { retries: { type: 'integer', default: 3 } },
additionalProperties: false,
}, { useDefaults: true });
const config = {};
validate(config);
console.log(config.retries); // 3This mutates the input object. Compilation throws when the schema cannot apply defaults without ambiguous behavior.
Strip properties forbidden by the schemaremove-extra-properties
const validate = validator({
type: 'object',
properties: { name: { type: 'string' } },
additionalProperties: false,
}, { removeAdditional: true });
const value = { name: 'Ada', admin: true };
validate(value);
console.log(value); // { name: 'Ada' }removeAdditional mutates data and supports additionalProperties: false and additionalItems: false; uncertain schema paths fail compilation.
Generate validator source during a buildgenerate-standalone-module
const validate = validator(schema, { mode: 'strong' });
const moduleSource = validate.toModule();
// Write moduleSource to a generated file in your build, then import that file at runtime.Pre-generating avoids runtime function construction under strict CSP. Treat schemas and custom format functions as trusted build inputs.
Collect schema compilation problemslint-schema
const { lint } = require('@exodus/schemasafe');
const errors = lint(schema, { mode: 'strong' });
for (const error of errors) {
console.error(error.keywordLocation, error.message);
}Linter mode is experimental, and its exact messages or details may change in non-major releases.
Follow newer format-annotation behaviorselect-spec-mode
const validate = validator(schema, { mode: 'spec' });For draft 2019-09 and newer, spec mode disables format assertions by default and relaxes unused or unreachable keyword checks; default and strong modes assert formats.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ajv | npm | Choose it for the largest JSON Schema ecosystem, broader extension support, standalone generation, and mature TypeScript integrations |
| jsonschema | npm | Choose it when a straightforward non-code-generating validator is preferable to maximum throughput |
| zod | npm | Choose it when TypeScript-first schema authoring and inferred application types matter more than JSON Schema portability |