🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@amritk/generate-validators

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amritk/generate-validators - npm Package Compare versions

Comparing version
0.8.0
to
0.9.0
+69
-12
dist/generators/generate-validator-function.js
import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
import { refToName } from '@amritk/helpers/ref-to-name';
import { safeAccessor } from '@amritk/helpers/safe-accessor';
import { hasAdditionalProperties, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaximum, hasMaxLength, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';

@@ -408,3 +409,4 @@ import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';

return null;
const raw = `${objAcc}[${JSON.stringify(key)}]`;
// Dotted access (`obj.number`) for identifier keys, bracket access otherwise.
const raw = safeAccessor(objAcc, key);
// Anything the slow path enforces past a typeof is cheaper to leave to the

@@ -451,3 +453,31 @@ // slow path than to mirror here, so bail and keep the guard sound.

};
/** A property key an array carries with a non-`undefined` value: `length`, or a
* canonical array index. A required prop on one of these can't be used to rule
* out arrays (an array's `length` is a number, an index can be anything). */
const ARRAY_INDEX_KEY = /^(0|[1-9]\d*)$/;
/** Schema types whose guard is a `typeof` check `typeof undefined` never passes. */
const TYPEOF_CHECKABLE_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object']);
/**
* Whether some required, typeof-guarded property proves the value can't be an
* array — letting the object shape-check drop its `!Array.isArray(...)` term. An
* array indexed by a normal key yields `undefined` (or an inherited method),
* which no `typeof === 'string' | 'number' | 'boolean' | 'object'` accepts, so
* that field check already rejects arrays. Keys an array does carry a real value
* for (`length`, numeric indices) are excluded, since those could slip through.
*/
const arrayRejectedByRequiredProp = (keys, required, properties) => {
for (const key of keys) {
if (!required.has(key) || key === 'length' || ARRAY_INDEX_KEY.test(key))
continue;
const propSchema = properties[key];
if (propSchema !== undefined &&
isSchemaObject(propSchema) &&
hasType(propSchema) &&
TYPEOF_CHECKABLE_TYPES.has(propSchema.type)) {
return true;
}
}
return false;
};
/**
* Builds the allocation-free boolean guard for an object schema as a list of

@@ -484,3 +514,6 @@ * `&&` conditions, or `null` when the schema can't be proven valid by a cheap

const keys = Object.keys(properties);
const conditions = [`typeof ${raw} === 'object' && ${raw} !== null && !Array.isArray(${raw})`];
// The object shape-check only needs `!Array.isArray` when no required field
// check would already reject an array (see `arrayRejectedByRequiredProp`).
const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? '' : ` && !Array.isArray(${raw})`;
const conditions = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
for (const key of keys) {

@@ -559,15 +592,14 @@ // An optional property would need an `=== undefined ||` branch and breaks

// well-typed (and, for strict objects, there are no extras) it returns true
// without allocating an `errors` array or walking the slow path. It returns
// without allocating an `errors` array or touching the slow path. It returns
// true only for provably valid input; anything it can't prove cheaply falls
// through to the error-collecting body below, which produces the same verdict
// and full JSON-Pointer errors. Schemas with constraints the guard can't
// express produce no guard at all (`null`), leaving behaviour unchanged.
// through to the error-collecting path, which produces the same verdict and
// full JSON-Pointer errors. Schemas with constraints the guard can't express
// produce no guard at all (`null`), leaving behaviour unchanged.
const guard = guardObjectConditions(schema, 'input', 'obj');
const guardBlock = guard
? [` if (`, guard.map((condition) => ` ${condition}`).join(' &&\n'), ` ) {`, ` return true`, ` }`, ``]
: [];
return [
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
// The cold, error-collecting body. When there's a guard this is a separate
// (unexported) function reached only on failure; the hot path never enters it
// unless input is actually invalid, so its size never costs the happy path.
const collectBody = (name, exported) => [
`${exported ? 'export ' : ''}const ${name} = (input: unknown, _path = ''): ValidationResult => {`,
` const obj = input as Record<string, unknown>`,
...guardBlock,
` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,

@@ -582,2 +614,27 @@ ` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,

].join('\n');
// No guard: the exported validator is the error-collecting function itself.
if (!guard) {
return `${hoistedBlock}${collectBody(vName, true)}`;
}
// With a guard, keep the happy path inside the exported function — the guard
// is inlined as an early `return true`, so a valid input never pays an extra
// call — and move only the cold, error-collecting body into a separate
// (unexported) function. That keeps `validateX` itself tiny (guard + a single
// tail call) so V8 optimises it well, without the giant error body bloating
// the hot path. The exported `(input, _path?) => ValidationResult` contract
// is unchanged.
const collectName = `${vName}Errors`;
return [
`${hoistedBlock}${collectBody(collectName, false)}`,
``,
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
` const obj = input as Record<string, unknown>`,
` if (`,
guard.map((condition) => ` ${condition}`).join(' &&\n'),
` ) {`,
` return true`,
` }`,
` return ${collectName}(input, _path)`,
`}`,
].join('\n');
};

@@ -584,0 +641,0 @@ /**

+4
-2
{
"name": "@amritk/generate-validators",
"version": "0.8.0",
"version": "0.9.0",
"description": "Generate TypeScript validation functions from JSON Schemas.",

@@ -36,3 +36,3 @@ "module": "./dist/index.js",

"test": "NODE_ENV=production vitest run --root ../.. generate-validators",
"bench": "bun run ./bench/run.ts"
"bench": "bun --conditions development ./bench/run.ts"
},

@@ -53,2 +53,3 @@ "imports": {

"devDependencies": {
"@ryoppippi/unplugin-typia": "^2.6.5",
"@scalar/openapi-parser": "^0.26.1",

@@ -58,4 +59,5 @@ "@sinclair/typebox": "^0.34.49",

"ajv-formats": "^3.0.1",
"typia": "^12.1.1",
"zod": "^4.4.3"
}
}
+27
-21

@@ -98,29 +98,35 @@ <div align="center">

Generated validators are straight-line, monomorphic TypeScript with no generic
dispatch. On the happy path they run a single allocation-free boolean guard — a
pure `&&` chain of `typeof` checks (plus an `Object.keys().length` count when an
object is closed with `additionalProperties: false`) — and only fall back to the
error-collecting body when something is actually wrong. That makes a valid-input
check as cheap as TypeBox's compiled checker while still emitting full
JSON-Pointer errors for invalid input, and emitting the validator stays far
cheaper than compiling a schema at startup. Measured on Bun 1.3 (Linux x64),
validating valid input at steady state:
dispatch. The exported `validateX` is split into a hot and a cold half: on the
happy path it runs a single allocation-free boolean guard — a pure `&&` chain of
`typeof` checks (plus an `Object.keys().length` count when an object is closed
with `additionalProperties: false`) — and `return true`s straight away, only
calling a separate error-collecting function when something is actually wrong.
Keeping the hot function tiny lets V8 optimise it aggressively, so a valid-input
check beats every other library measured — including the build-time transformer
typia — while still emitting full JSON-Pointer errors for invalid input, and
emitting the validator stays far cheaper than compiling a schema at startup.
Measured on Bun 1.3 (Linux x64), validating valid input at steady state:
| schema | mjst (generated) | ajv (compiled) | typebox (compiled) | zod |
|:--|--:|--:|--:|--:|
| small (4 fields) | **~37M** ops/s | ~10M ops/s | ~4.9M ops/s | ~2.0M ops/s |
| order (nested + array) | **~11M** ops/s | ~3.7M ops/s | ~2.0M ops/s | ~0.5M ops/s |
| assert-loose | **~67M** ops/s | ~40M ops/s | ~57M ops/s | ~3.2M ops/s |
| assert-strict | **~47M** ops/s | ~19M ops/s | ~36M ops/s | ~1.3M ops/s |
| schema | mjst (generated) | typia (transformed) | ajv (compiled) | typebox (compiled) | zod |
|:--|--:|--:|--:|--:|--:|
| small (4 fields) | **~22M** ops/s | ~4.2M ops/s | ~7.0M ops/s | ~4.0M ops/s | ~1.8M ops/s |
| order (nested + array) | **~6.9M** ops/s | ~1.7M ops/s | ~2.5M ops/s | ~1.7M ops/s | ~0.4M ops/s |
| assert-loose | **~110M** ops/s | ~100M ops/s | ~31M ops/s | ~41M ops/s | ~3.2M ops/s |
| assert-strict | **~98M** ops/s | ~82M ops/s | ~13M ops/s | ~28M ops/s | ~1.1M ops/s |
The `assert-loose` / `assert-strict` rows are the exact shape used by
[`moltar/typescript-runtime-type-benchmarks`](https://github.com/moltar/typescript-runtime-type-benchmarks)
(seven scalar roots plus a nested object); the boolean guard lets mjst edge out
TypeBox's compiled checker on both, with and without `additionalProperties:
false`.
(seven scalar roots plus a nested object); the boolean guard lets mjst edge past
typia on both, with and without `additionalProperties: false`. (typia and
TypeBox still win the *invalid* path, where they bail on the first error rather
than collecting a full error list.)
Preparing a validator costs ~0.1 ms for mjst codegen and ~0.05–0.12 ms for a
TypeBox `TypeCompiler` compile, versus ~8–10 ms for an Ajv compile. All four
libraries agree on every verdict; parity is asserted before timing (TypeBox is
given uuid/email format checkers so every library does the same work).
Micro-benchmark figures vary by machine and runtime — reproduce with:
TypeBox `TypeCompiler` compile, versus ~8–10 ms for an Ajv compile. Every library
agrees on every verdict; parity is asserted before timing (TypeBox is given
uuid/email format checkers so every library does the same work). 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 and the
numbers stay reproducible. Micro-benchmark figures vary by machine and runtime —
reproduce with:

@@ -127,0 +133,0 @@ ```bash