@amritk/generate-validators 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -2
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -247,7 +247,7 @@ happy path it runs a single allocation-free boolean guard — a pure `&&` chain
|
|
|
247
247
|
`typeof` checks (plus an `Object.keys().length` count when an object is closed
|
|
248
248
|
with `additionalProperties: false`) — and `return true`s straight away, only
|
|
249
249
|
calling a separate error-collecting function when something is actually wrong.
|
|
250
|
-
Keeping the hot function tiny lets
|
|
250
|
+
Keeping the hot function tiny lets the JIT optimise it aggressively, so a valid-input
|
|
251
251
|
check beats every other library measured — including the build-time transformer
|
|
252
252
|
typia — while still emitting full JSON-Pointer errors for invalid input, and
|
|
253
253
|
emitting the validator stays far cheaper than compiling a schema at startup.
|
|
@@ -260,7 +260,7 @@ Measured on Bun 1.4 (Linux x64), validating valid input at steady state:
|
|
|
260
260
|
| assert-loose | **~189M** ops/s | ~170M ops/s | ~44M ops/s | ~78M ops/s | ~5M ops/s |
|
|
261
261
|
| assert-strict | **~104M** ops/s | ~68M ops/s | ~21M ops/s | ~42M ops/s | ~1.8M ops/s |
|
|
262
262
|
|
|
263
|
-
The `assert-loose` / `assert-strict` rows
|
|
263
|
+
The `assert-loose` / `assert-strict` rows use the same *shape* as
|
|
264
264
|
[`moltar/typescript-runtime-type-benchmarks`](https://github.com/moltar/typescript-runtime-type-benchmarks)
|
|
265
265
|
(seven scalar roots plus a nested object): the boolean guard keeps mjst ahead of
|
|
266
266
|
typia on both, by ~11% on `assert-loose` — close enough that the two can trade
|
|
@@ -269,6 +269,12 @@ the lead run-to-run — and by ~52% on `assert-strict` (with
|
|
|
269
269
|
(typia and TypeBox still win the *invalid* path, where they bail on the first
|
|
270
270
|
error rather than collecting a full error list.)
|
|
271
271
|
|
|
272
|
+
They are **not** that project's numbers and they do not belong next to its
|
|
273
|
+
leaderboard: the shape is shared, the harness is not, and the harness is worth
|
|
274
|
+
an order of magnitude. See
|
|
275
|
+
[Against the moltar harness](#against-the-moltar-harness) for the same functions
|
|
276
|
+
measured under benny, the way the leaderboard measures them.
|
|
277
|
+
|
|
272
278
|
Preparing a validator costs ~0.3–0.7 ms for mjst codegen and ~0.05–0.2 ms for a
|
|
273
279
|
TypeBox `TypeCompiler` compile, versus ~13–17 ms for an Ajv compile. Every library
|
|
274
280
|
agrees on every verdict; parity is asserted before timing.
|
|
@@ -289,6 +295,94 @@ reproduce with:
|
|
|
289
295
|
bun run bench
|
|
290
296
|
```
|
|
291
297
|
|
|
298
|
+
### Against the moltar harness
|
|
299
|
+
|
|
300
|
+
The table above is this package's own harness (`bench/measure.ts`): the
|
|
301
|
+
validator is called directly over a pool of 32 distinct inputs, its verdict is
|
|
302
|
+
folded into an escaping sink so nothing can be optimised away, and the median of
|
|
303
|
+
21 timed trials is reported.
|
|
304
|
+
|
|
305
|
+
The public leaderboard measures differently. Every operation there goes through
|
|
306
|
+
[benny](https://github.com/caderek/benny) (benchmark.js) into moltar's
|
|
307
|
+
`Benchmark` class, so the timed work is a call inside benchmark.js's compiled
|
|
308
|
+
loop, then a second call through a class property, around a fixture that is one
|
|
309
|
+
shared frozen module-level constant whose verdict `run()` throws away. That
|
|
310
|
+
harness has a floor, and near the top of the range the floor is what gets
|
|
311
|
+
measured.
|
|
312
|
+
|
|
313
|
+
`bun run bench:moltar` runs exactly that harness over the same functions, always
|
|
314
|
+
alongside a **no-op** control — a "validator" that checks nothing, which is the
|
|
315
|
+
fastest number the harness can physically produce. One run on this machine
|
|
316
|
+
(Linux x64, Bun 1.3.11 / Node 22.22, valid input):
|
|
317
|
+
|
|
318
|
+
| harness | runtime | `assert-loose` | `assert-strict` |
|
|
319
|
+
|:--|:--|--:|--:|
|
|
320
|
+
| this package (`measure.ts`) | Bun | ~200M ops/s | ~185M ops/s |
|
|
321
|
+
| benny, moltar's `Benchmark` | Node | ~100M ops/s | ~38M ops/s |
|
|
322
|
+
| benny, moltar's `Benchmark` | Bun | ~70M ops/s | ~2.4M ops/s |
|
|
323
|
+
| *no-op control, benny* | *Node* | *~120M ops/s* | *~120M ops/s* |
|
|
324
|
+
| *no-op control, benny* | *Bun* | *~325M ops/s (±75%)* | *~325M ops/s (±75%)* |
|
|
325
|
+
|
|
326
|
+
Read two things out of that. First, on Node the `assert-loose` figure sits
|
|
327
|
+
within 20% of a validator that does nothing, so under that harness it is not a
|
|
328
|
+
validator measurement at all: above that floor a faster validator cannot show
|
|
329
|
+
up as a faster number, and the published leaderboard runs on CI hardware slower
|
|
330
|
+
than this box, where the floor sits lower still. Second, `assert-strict` on Bun collapses to
|
|
331
|
+
~2.4M because moltar's fixture is `Object.freeze({ … })` — see
|
|
332
|
+
[Frozen inputs](#frozen-inputs).
|
|
333
|
+
|
|
334
|
+
The harness makes no difference to correctness and every difference to the
|
|
335
|
+
number, so quote the two separately or not at all. Both are reproducible here:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
bun run bench # this package's harness
|
|
339
|
+
bun run bench:moltar # benny, under the leaderboard's conditions
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Frozen inputs
|
|
343
|
+
|
|
344
|
+
Closing an object with `additionalProperties: false` means proving no
|
|
345
|
+
undeclared key is present, and every library answers that by enumerating keys:
|
|
346
|
+
mjst's guard counts them (`Object.keys(obj).length === n`, exact because each
|
|
347
|
+
declared property is required and already proven present), Ajv and Zod sweep
|
|
348
|
+
with `for...in`, TypeBox runs its own sweep. On V8 that costs the same whatever
|
|
349
|
+
the input looks like.
|
|
350
|
+
|
|
351
|
+
On JavaScriptCore (Bun) it does not. Making an object non-extensible —
|
|
352
|
+
`Object.freeze`, `Object.seal` or a bare `Object.preventExtensions` — turns off
|
|
353
|
+
the engine's cached own-keys fast path, and *every* form of key enumeration
|
|
354
|
+
falls back to a generic walk: `Object.keys`, `Object.getOwnPropertyNames`,
|
|
355
|
+
`Reflect.ownKeys` and `for...in` alike. Property reads are untouched (a frozen
|
|
356
|
+
object reads at full speed), so the whole cost lands on the extra-key sweep, and
|
|
357
|
+
therefore on strict schemas only. Frozen inputs are ordinary — a config object
|
|
358
|
+
frozen at startup, a shared fixture, a module-level constant — so `bun run bench`
|
|
359
|
+
carries `small (4 fields, frozen)` and `assert-strict (frozen)` cases to keep it
|
|
360
|
+
measured. One run on this machine (Linux x64, Bun 1.3.11), valid input:
|
|
361
|
+
|
|
362
|
+
| `assert-strict` | mutable input | frozen input |
|
|
363
|
+
|:--|--:|--:|
|
|
364
|
+
| mjst (generated) | ~185M ops/s | ~1.7M ops/s |
|
|
365
|
+
| typia (transformed) | ~68M ops/s | ~1.7M ops/s |
|
|
366
|
+
| typebox (compiled) | ~46M ops/s | ~1.7M ops/s |
|
|
367
|
+
| ajv (compiled) | ~24M ops/s | ~1.5M ops/s |
|
|
368
|
+
| zod | ~1.4M ops/s | ~0.7M ops/s |
|
|
369
|
+
|
|
370
|
+
It is an engine-level cliff, not an mjst one: every compiled or generated strict
|
|
371
|
+
validator lands within a hair of the same number, because they are all paying
|
|
372
|
+
the same engine slow path. The generated code keeps the key count anyway. Every
|
|
373
|
+
alternative was measured and every one is worse overall. `Object.values(obj)`
|
|
374
|
+
and `Object.keys({ ...obj })` sidestep the cliff, but on the ordinary mutable
|
|
375
|
+
path they cost 28–37× under JSC and 2–7× under V8. Branching on
|
|
376
|
+
`Object.isExtensible(obj)` first keeps the mutable path recognisable, at ~4×
|
|
377
|
+
under JSC — and makes V8 slower in *both* directions (~2× mutable, ~7× frozen),
|
|
378
|
+
where there was no cliff to fix in the first place. Trading a large, portable
|
|
379
|
+
regression for a smaller win on one engine is not a good deal, so the sweep
|
|
380
|
+
stays as it is.
|
|
381
|
+
|
|
382
|
+
If it matters for your workload: validate before freezing (the verdict is the
|
|
383
|
+
same either way — `src/generators/frozen-input.test.ts` pins that), or run on a
|
|
384
|
+
V8 runtime, where the cliff does not exist.
|
|
385
|
+
|
|
292
386
|
---
|
|
293
387
|
|
|
294
388
|
## Related packages
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-validators",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Generate TypeScript validation functions from JSON Schemas.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"prepublishOnly": "node ../../scripts/check-publishable.mjs",
|
|
42
42
|
"types:check": "tsgo -p . --noEmit",
|
|
43
43
|
"test": "NODE_ENV=production vitest run --root ../.. generate-validators",
|
|
44
|
-
"bench": "bun --conditions development ./bench/run.ts"
|
|
44
|
+
"bench": "bun --conditions development ./bench/run.ts",
|
|
45
|
+
"bench:moltar": "bun --conditions development ./bench/moltar.ts"
|
|
45
46
|
},
|
|
46
47
|
"imports": {
|
|
47
48
|
"#generators/*": "./src/generators/*.ts"
|
|
@@ -54,15 +55,16 @@
|
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
56
57
|
"json-schema-typed": "^8.0.1",
|
|
57
|
-
"@amritk/helpers": "^0.
|
|
58
|
+
"@amritk/helpers": "^0.17.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
|
-
"@amritk/runtime-validators": "^0.12.
|
|
61
|
+
"@amritk/runtime-validators": "^0.12.1",
|
|
61
62
|
"@ryoppippi/unplugin-typia": "^2.6.5",
|
|
62
63
|
"@scalar/openapi-parser": "^0.26.1",
|
|
63
64
|
"@sinclair/typebox": "^0.34.49",
|
|
64
65
|
"ajv": "^8.17.1",
|
|
65
66
|
"ajv-formats": "^3.0.1",
|
|
67
|
+
"benny": "^3.7.1",
|
|
66
68
|
"typia": "^12.1.1",
|
|
67
69
|
"zod": "^4.4.3"
|
|
68
70
|
}
|