@amritk/generate-parsers 0.20.1 → 0.21.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/AI.md CHANGED
@@ -22,9 +22,11 @@ const files = await buildSchema(schema, 'Document')
22
22
 
23
23
  1. **`buildSchema` is `async` and takes POSITIONAL args, no options object.** The
24
24
  full signature is
25
- `buildSchema(rootSchema, rootTypeName, extensions?, typesOnly?, logWarnings?, strict?, helpersMode?, helpersImportPrefix?, readonly?, stripUnknown?, typeSuffix?, importExt?, caseInsensitive?)`.
25
+ `buildSchema(rootSchema, rootTypeName, extensions?, typesOnly?, logWarnings?, strict?, helpersMode?, helpersImportPrefix?, readonly?, stripUnknown?, typeSuffix?, importExt?, caseInsensitive?, schemas?, unknownKeys?)`.
26
26
  To set a later flag you must pass every intervening positional. (The README's
27
- short form omits the trailing seven.)
27
+ short form omits the trailing seven.) `unknownKeys` (`'count-keys'` by default,
28
+ `'count-enumerable'` for Node-only output) picks how a closed object's fast
29
+ path counts keys; nothing in the generated code detects its runtime.
28
30
  2. **It returns files in memory — it does NOT write to disk.** You write the
29
31
  `GeneratedFile[]` (`{ filename, content }`) yourself. `rootTypeName` becomes
30
32
  that filename (lowercased) as well as the `export type` name, so it must be a
package/README.md CHANGED
@@ -95,6 +95,7 @@ Every option after the first two is **positional** — pass them in this order:
95
95
  | 12 | `importExt` | `'js' \| 'ts'` | `'js'` | Extension emitted on relative import specifiers. `'js'` is the NodeNext form for output you compile; `'ts'` emits the literal on-disk paths so the sources run unbuilt. |
96
96
  | 13 | `caseInsensitive` | `boolean` | `false` | Normalize a mis-cased string to the exact casing of an enum/const member it matches case-insensitively. Coerce mode only. |
97
97
  | 14 | `schemas` | `Record<string, unknown>` | — | Documents you have **already loaded**, keyed by the absolute URI a `$ref` names them by. See below. |
98
+ | 15 | `unknownKeys` | `'count-keys' \| 'count-enumerable'` | `'count-keys'` | How a fast path proves a closed object (`additionalProperties: false`, or a `stripUnknown` build) has no undeclared key: a prototype-guarded `Object.keys(input).length` (fastest on Bun) or a `for…in` count (fastest on Node). See [Counting keys on the fast path](#counting-keys-on-the-fast-path). |
98
99
 
99
100
  Returns: `Promise<GeneratedFile[]>`.
100
101
 
@@ -184,6 +185,14 @@ nobody registered still stops the build, with a message naming the ref.
184
185
  <tr>
185
186
  <td colspan="3">Normalize a mis-cased string to the exact casing of a declared enum/const member it matches case-insensitively (e.g. hElLo → hello) instead of coercing to the default. Coerce mode only — strict parsers still reject a casing mismatch. Correctly-cased input keeps the exact-match fast path, so the hot path is unaffected.</td>
186
187
  </tr>
188
+ <tr>
189
+ <td>🔢 <code>unknownKeys</code></td>
190
+ <td><code>string</code></td>
191
+ <td align="center"><code>"count-keys"</code></td>
192
+ </tr>
193
+ <tr>
194
+ <td colspan="3">How a fast path proves a closed object (additionalProperties: false, or a stripUnknown build) carries no undeclared key. 'count-keys' is a prototype-guarded Object.keys(input).length comparison, the faster form on JavaScriptCore (Bun); 'count-enumerable' is a for…in count that allocates nothing; it was the faster form on Node 22, but on Node 26 'count-keys' ties or wins there too. Keep the default unless you are pinned to an older V8 and have measured your own shapes; nothing detects the runtime at runtime.<br><strong>Allowed:</strong> <code>"count-enumerable"</code>, <code>"count-keys"</code></td>
195
+ </tr>
187
196
  </tbody>
188
197
  </table>
189
198
  <!-- config-table-end -->
@@ -237,9 +246,10 @@ Three deliberate departures:
237
246
  Ajv accepts `"a string"` against `{ properties: { a: {} } }`; a parser has to
238
247
  return the type it declares, and the declared type is an object.
239
248
 
240
- If a strict parser cannot enforce something — a `$ref` cycle it would have to
241
- inline because the build emits a single file, a subschema using a keyword it
242
- cannot prove — generation **fails with an error naming the keyword** rather than
249
+ If a strict parser cannot enforce something — a `$ref` cycle inside a subschema it
250
+ has to match inline (`contains`, `not`, `propertyNames`, `dependentSchemas`, or an
251
+ `unevaluated*` backstop), which the inline matcher will not unroll; a subschema
252
+ using a keyword it cannot prove — generation **fails with an error naming the keyword** rather than
243
253
  emitting a parser that quietly accepts what the schema forbids. Coercing
244
254
  (non-strict) parsers are documented to repair rather than reject, so they ignore
245
255
  the rejecting keywords by design.
@@ -266,8 +276,9 @@ no I/O to answer the retrieval step. Everything else — applying the base URIs,
266
276
  walking anchors across documents, emitting a parser per definition and an import
267
277
  graph that links — the generator still has to do.
268
278
 
269
- Of the 41 that do not, **18** are the `ignores a non-object` cases that follow
270
- from the third departure above, and **12** are a keyword strict mode will not
279
+ Of the 41 that do not, **18** follow from the third departure above — the
280
+ `ignores a non-object` cases plus three `ref.json` cases where a recursive `$ref`
281
+ lands on a root that declares `properties` — and **12** are a keyword strict mode will not
271
282
  approximate (a cyclic `$ref` with siblings, a cyclic `unevaluatedProperties`) —
272
283
  those cost a build error naming the cause, never a wrong verdict. The rest: **8**
273
284
  `$dynamicRef`s whose binding depends on the evaluation path (a generator emits one
@@ -296,20 +307,34 @@ keys) and `parseStrict` (assert + reject undeclared keys) halves of
296
307
  [`moltar/typescript-runtime-type-benchmarks`](https://github.com/moltar/typescript-runtime-type-benchmarks)
297
308
  against the other *pure* parsers — the ones that return a new typed value rather
298
309
  than mutating in place — [zod](https://zod.dev) and
299
- [TypeBox](https://github.com/sinclairzx81/typebox). Measured on Bun 1.4 (Linux
300
- x64), parsing valid input at steady state:
301
-
302
- | case | mode | mjst (generated) | zod | typebox |
303
- |:--|:--|--:|--:|--:|
304
- | user (4 fields) | parseSafe | **~18M** ops/s | ~3.7M ops/s | ~1.6M ops/s |
305
- | order (nested + array) | parseSafe | **~6.9M** ops/s | ~0.71M ops/s | ~0.24M ops/s |
306
- | user (4 fields) | parseStrict | **~14M** ops/s | ~2.3M ops/s | ~2.04M ops/s |
307
- | order (nested + array) | parseStrict | **~7M** ops/s | ~0.43M ops/s | ~0.36M ops/s |
308
-
309
- The upstream `assert` case (seven scalar roots plus a nested object) runs faster
310
- still — ~51M ops/s strict, ~127M safe — but at that size the numbers swing enough
311
- run-to-run that the ratio, not the absolute, is the honest signal: mjst lands
312
- ~5–29× ahead of zod across every case above.
310
+ [TypeBox](https://github.com/sinclairzx81/typebox).
311
+
312
+ Both runtimes were measured together on one machine (Linux x64, a 4-vCPU cloud
313
+ box, Bun 1.4.0 and Node 26.8.1 — the same machine and runtimes as every table in
314
+ this repo), each cell the median of three separate runs of the whole suite.
315
+ Within one sitting a cell repeats to within a few percent; the same suite
316
+ measured hours later moved ~60% across every case at once, so read the ratios
317
+ and treat the absolutes as a property of that box.
318
+
319
+ Parsing valid input at steady state:
320
+
321
+ | case | mode | Bun: mjst | Bun: zod | Bun: typebox | Node: mjst | Node: zod | Node: typebox |
322
+ |:--|:--|--:|--:|--:|--:|--:|--:|
323
+ | user (4 fields) | parseSafe | **~163M** | ~3.3M | ~1.7M | **~88M** | ~4.0M | ~0.67M |
324
+ | order (nested + array) | parseSafe | **~7.7M** | ~0.60M | ~0.26M | **~8.2M** | ~0.64M | ~0.15M |
325
+ | assert (moltar shape) | parseSafe | **~120M** | ~3.5M | ~0.87M | **~66M** | ~5.3M | ~0.35M |
326
+ | user (4 fields) | parseStrict | **~43M** | ~1.9M | ~2.2M | **~55M** | ~2.7M | ~1.5M |
327
+ | order (nested + array) | parseStrict | **~13M** | ~0.36M | ~0.43M | **~8.7M** | ~0.55M | ~0.25M |
328
+ | assert (moltar shape) | parseStrict | **~44M** | ~1.4M | ~1.2M | **~34M** | ~3.5M | ~0.80M |
329
+
330
+ <sub>ops/s, higher is better.</sub>
331
+
332
+ Unlike the validator suite — where TypeBox's compiled checker takes the loose
333
+ moltar shape on V8 — the generated parser leads every case on both engines:
334
+ ~12–49× over zod on Bun and ~13–22× on Node. The two `user`/`assert` safe cells
335
+ are the ones to read as ratios rather than absolutes: stripping to four declared
336
+ keys builds one small object and nothing else, which is fast enough that the
337
+ engine's inlining, not the parser, sets the number.
313
338
 
314
339
  What is replicated upstream is the *cases* — the two parse modes and their
315
340
  shapes — not the harness. These numbers come from this repo's own measurement
@@ -321,9 +346,9 @@ reproducible run of both harnesses:
321
346
  [Against the moltar harness](../generate-validators#against-the-moltar-harness).
322
347
 
323
348
  The trade is a one-shot **prepare** cost that only mjst pays — generating the
324
- parser source — which measures **~0.2–0.8 ms** per schema here (zod and TypeBox
325
- author or interpret their parsers with no separate build step, so there is
326
- nothing to time). That is trivially amortized: you generate once at build time
349
+ parser source — which measures **~0.20–0.74 ms** per schema on Bun and
350
+ **~0.16–0.72 ms** on Node (zod and TypeBox author or interpret their parsers
351
+ with no separate build step, so there is nothing to time). That is trivially amortized: you generate once at build time
327
352
  and run the emitted code forever.
328
353
 
329
354
  Every library is checked for agreement — same stripped/rejected output, same
@@ -333,9 +358,48 @@ the optimiser can't hoist the work away. Micro-benchmark figures vary by machine
333
358
  and runtime — reproduce with:
334
359
 
335
360
  ```bash
336
- bun run bench
361
+ bun run bench # Bun / JavaScriptCore
362
+ bun run bench:node # Node / V8 (builds the package first, then runs it under node)
337
363
  ```
338
364
 
365
+ ### Counting keys on the fast path
366
+
367
+ A closed object whose declared properties are all required proves "no undeclared
368
+ key" by counting: the typed checks ahead of the count have already proven every
369
+ declared key present, so exactly *n* keys means no extra. `unknownKeys` (the 15th
370
+ positional argument, `--unknown-keys` on the CLI) picks how the count is spelled,
371
+ because the two spellings trade places between the engines:
372
+
373
+ - **`'count-keys'`** (default) — `Object.getPrototypeOf(input) === Object.prototype
374
+ && Object.keys(input).length === n`, one more term of the fast-path guard. The
375
+ prototype guard keeps an own-key count sound: a crafted prototype could satisfy
376
+ the typed checks through an inherited declared key while an own extra kept the
377
+ count at *n*, so a non-plain object takes the cold path instead, whose `for…in`
378
+ rejection sees the inherited key.
379
+ - **`'count-enumerable'`** — `let c = 0; for (const k in input) c++`, as
380
+ statements behind the guard. It allocates nothing and needs no prototype guard
381
+ (the count sees inherited keys too), and on V8 it is answered from the enum
382
+ cache. On JavaScriptCore a `for…in` over a non-extensible object is the engine's
383
+ slow path, and the strict parse runs at half speed.
384
+
385
+ A nested object's shape is proven on the parent's fast path by the same checks
386
+ its own shape validator makes, spelled out over the cached local rather than
387
+ called — `typeof _nested === "object" && … && Object.keys(_nested).length === n`
388
+ — one level deep and within a size budget, because a call was the one thing
389
+ left on the strict fast path that JavaScriptCore would not see through. Under
390
+ the moltar harness (each case alone in its own process) `parseStrict` reaches
391
+ the harness floor on Bun 1.4.0 (~360M ops/s, the call eliminated) with
392
+ `count-keys` against ~22M with `count-enumerable`.
393
+
394
+ On Node the answer has moved with the engine. On Node 22 the two spellings were
395
+ level (~14M either way, which is where the advice to flip it came from); on
396
+ Node 26.8.1 `count-keys` measures ~29M against `count-enumerable`'s ~26M, so the
397
+ default now ties or wins on both engines. Keep the default unless you are
398
+ pinned to an older V8 and have measured your own shapes. See
399
+ [Choosing how keys are counted](../generate-validators#choosing-how-keys-are-counted)
400
+ in the sister package for the validator numbers and the full trade-off. Whichever
401
+ you pick, every verdict on a value that could have come from JSON is the same.
402
+
339
403
  ---
340
404
 
341
405
  ## Related packages
@@ -1,3 +1,4 @@
1
+ import { type UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
1
2
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
3
  import type { HelpersMode } from '../helpers/collect-helpers.js';
3
4
  import type { ImportExtension } from '../helpers/collect-imports.js';
@@ -70,6 +71,11 @@ export type GeneratedFile = {
70
71
  * is yours to do (or `@amritk/resolve-refs`'), and a `$ref` to a URI nobody registered still
71
72
  * stops generation. Only the documents actually referenced get files, so registering more
72
73
  * than the schema uses costs nothing in the output.
74
+ * @param unknownKeys - How the generated fast paths prove a closed object
75
+ * (`additionalProperties: false`, or a `stripUnknown` build) carries no undeclared key:
76
+ * `'count-keys'` (the default) compares `Object.keys(input).length` behind a plain-prototype
77
+ * guard, `'count-enumerable'` counts with `for…in`. The first is the faster form on
78
+ * JavaScriptCore (Bun), the second on V8 (Node) — see the README for the measurements.
73
79
  * @returns An array of generated TypeScript files
74
80
  *
75
81
  * @example
@@ -102,4 +108,4 @@ export type GeneratedFile = {
102
108
  * });
103
109
  * ```
104
110
  */
105
- export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean, stripUnknown?: boolean, typeSuffix?: string, importExt?: ImportExtension, caseInsensitive?: boolean, schemas?: Readonly<Record<string, unknown>>) => Promise<GeneratedFile[]>;
111
+ export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean, stripUnknown?: boolean, typeSuffix?: string, importExt?: ImportExtension, caseInsensitive?: boolean, schemas?: Readonly<Record<string, unknown>>, unknownKeys?: UnknownKeysStrategy) => Promise<GeneratedFile[]>;
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { dirname, resolve as resolvePath } from "node:path";
4
4
  import { foldNullable } from "@amritk/helpers/fold-nullable";
5
5
  import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel";
6
+ import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
6
7
  import { walkRefGraph } from "@amritk/helpers/walk-ref-graph";
7
8
  import { applySchemaExtensions } from "../helpers/apply-schema-extensions.js";
8
9
  import { generateFile } from "./generate-files.js";
@@ -26,7 +27,7 @@ const assertRootTypeName = (rootTypeName) => {
26
27
  return;
27
28
  throw new Error(`Invalid root type name ${JSON.stringify(rootTypeName)}: expected a TypeScript identifier. The name is emitted as \`export type <name>\` and, lowercased, used as the output filename, so anything else either fails to compile or escapes the output directory. Derive it with @amritk/helpers' \`deriveRootTypeName\`, which always produces one.`);
28
29
  };
29
- const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false, stripUnknown = false, typeSuffix = "", importExt = "js", caseInsensitive = false, schemas) => {
30
+ const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false, stripUnknown = false, typeSuffix = "", importExt = "js", caseInsensitive = false, schemas, unknownKeys = DEFAULT_UNKNOWN_KEYS) => {
30
31
  assertRootTypeName(rootTypeName);
31
32
  const files = [];
32
33
  const usedHelpers = /* @__PURE__ */ new Set();
@@ -49,7 +50,8 @@ const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logW
49
50
  ...logWarnings !== void 0 ? { logWarnings } : {},
50
51
  ...strict !== void 0 ? { strict } : {},
51
52
  ...stripUnknown ? { stripUnknown } : {},
52
- ...caseInsensitive ? { caseInsensitive } : {}
53
+ ...caseInsensitive ? { caseInsensitive } : {},
54
+ unknownKeys
53
55
  });
54
56
  files.push({ filename: `${node.filename}.ts`, content: result.content });
55
57
  for (const helper of result.usedHelpers)
@@ -1,3 +1,4 @@
1
+ import { type UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
1
2
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
3
  import { type HelpersMode, type RuntimeHelperName } from '../helpers/collect-helpers.js';
3
4
  import { type ImportExtension } from '../helpers/collect-imports.js';
@@ -92,6 +93,12 @@ type GenerateFileOptions = {
92
93
  * already-valid input keeps the exact-match fast path.
93
94
  */
94
95
  readonly caseInsensitive?: boolean;
96
+ /**
97
+ * How the generated fast paths prove a closed object carries no undeclared
98
+ * key — `Object.keys` (the default) or a `for…in` count. See
99
+ * {@link UnknownKeysStrategy} for the trade-off between the two.
100
+ */
101
+ readonly unknownKeys?: UnknownKeysStrategy;
95
102
  };
96
103
  /** Result of generating a single parser file. */
97
104
  export type GeneratedFileContent = {
@@ -1,4 +1,5 @@
1
1
  import { generateTypeDefinition } from "@amritk/helpers/generate-type-definition";
2
+ import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
2
3
  import { collectHelpers } from "../helpers/collect-helpers.js";
3
4
  import { collectImports, collectImportTypeNames } from "../helpers/collect-imports.js";
4
5
  import { generateParserFunction, generateShapeValidator } from "./generate-parser-function.js";
@@ -40,12 +41,14 @@ const generateFile = (schema, typeName, options) => {
40
41
  rootSchema,
41
42
  typeSuffix
42
43
  });
43
- const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown, reservedNames);
44
+ const unknownKeys = options?.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
45
+ const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown, reservedNames, unknownKeys);
44
46
  const parserFunction = generateParserFunction(schema, typeName, {
45
47
  useRefImports: true,
46
48
  typeSuffix,
47
49
  reservedNames,
48
50
  shapeValidatorSource: shapeValidator,
51
+ unknownKeys,
49
52
  ...options?.isRoot !== void 0 ? { isRoot: options.isRoot } : {},
50
53
  ...rootSchema !== void 0 ? { rootSchema } : {},
51
54
  ...options?.logWarnings !== void 0 ? { logWarnings: options.logWarnings } : {},
@@ -1,3 +1,4 @@
1
+ import { type UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
1
2
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
3
  /**
3
4
  * Options for controlling parser function generation behavior.
@@ -67,6 +68,13 @@ type GenerateParserOptions = {
67
68
  * exact `===` fast path and the hot path is unaffected.
68
69
  */
69
70
  readonly caseInsensitive?: boolean;
71
+ /**
72
+ * How the fast paths prove a closed object carries no undeclared key —
73
+ * `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length`
74
+ * (the default) or a `for…in` count. See {@link UnknownKeysStrategy} for the
75
+ * trade-off, and {@link keyCountCheck} for what each form emits.
76
+ */
77
+ readonly unknownKeys?: UnknownKeysStrategy;
70
78
  /**
71
79
  * The exact source of the `validate{TypeName}Shape` predicate emitted into
72
80
  * the same file as this parser (generate-files produces it for the root
@@ -108,5 +116,5 @@ export declare const shapeValidatorFunctionName: (typeName: string) => string;
108
116
  * in additionalProperties). The stub is safe: parents calling it will fall
109
117
  * through to the slow path, matching the pre-deep-fast-path behavior.
110
118
  */
111
- export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean, stripUnknown?: boolean, reservedNames?: ReadonlySet<string>) => string;
119
+ export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean, stripUnknown?: boolean, reservedNames?: ReadonlySet<string>, unknownKeys?: UnknownKeysStrategy) => string;
112
120
  export {};
@@ -5,6 +5,7 @@ import { resolveRef } from "@amritk/helpers/resolve-ref";
5
5
  import { missingCheck, safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
6
6
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasMaxItems, hasMaxProperties, hasMinItems, hasMinProperties, hasOneOf, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
7
7
  import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
8
+ import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
8
9
  import { findDiscriminator } from "../helpers/find-discriminator.js";
9
10
  import { getDefaultValue } from "../helpers/get-default-value.js";
10
11
  import { getDiscriminatorValue } from "../helpers/get-discriminator-value.js";
@@ -177,6 +178,11 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
177
178
  if (isSchemaObject(schema) && ("if" in schema || "then" in schema || "else" in schema)) {
178
179
  return `{} as ${typeName}`;
179
180
  }
181
+ if (isSchemaObject(schema) && hasAllOf(schema)) {
182
+ const composesConditional = schema.allOf.some((member) => isSchemaObject(member) && (hasRef(member) || "if" in member || "then" in member || "else" in member));
183
+ if (composesConditional)
184
+ return `{} as ${typeName}`;
185
+ }
180
186
  const requiredProps = [];
181
187
  for (const [key, propSchema] of Object.entries(schema.properties)) {
182
188
  const isRequired = isPropertyRequired(key, schema);
@@ -250,7 +256,8 @@ const strictContext = (ctx) => ({
250
256
  useRefImports: ctx?.useRefImports ?? false,
251
257
  suffix: ctx?.suffix ?? "",
252
258
  ...ctx?.rootSchema !== void 0 ? { rootSchema: ctx.rootSchema } : {},
253
- ...ctx?.stripUnknown !== void 0 ? { stripUnknown: ctx.stripUnknown } : {}
259
+ ...ctx?.stripUnknown !== void 0 ? { stripUnknown: ctx.stripUnknown } : {},
260
+ ...ctx?.unknownKeys !== void 0 ? { unknownKeys: ctx.unknownKeys } : {}
254
261
  });
255
262
  const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
256
263
  const functionName = generateParserName(typeName);
@@ -368,11 +375,12 @@ ${delegatedBody(itemParserName)}
368
375
  const useRefImports = unionCtx?.useRefImports ?? false;
369
376
  const suffix = unionCtx?.suffix ?? "";
370
377
  const stripUnknown = unionCtx?.stripUnknown ?? false;
371
- const itemShapeValidator = generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved);
378
+ const unknownKeys = unionCtx?.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
379
+ const itemShapeValidator = generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved, unknownKeys);
372
380
  const preamble = [
373
381
  `type ${itemName} = NonNullable<${typeName}>[number];`,
374
382
  itemShapeValidator,
375
- generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false, itemShapeValidator)
383
+ generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false, itemShapeValidator, unknownKeys)
376
384
  ].join("\n\n");
377
385
  return `${preamble}
378
386
 
@@ -548,13 +556,21 @@ const exactKeyCountOf = (schema) => {
548
556
  return null;
549
557
  return declaredCount;
550
558
  };
559
+ const KEY_COUNT_VAR = "_keyCount";
560
+ const keyCountCheck = (unknownKeys, count, indent, obj = "input") => unknownKeys === "count-keys" ? {
561
+ statements: [],
562
+ verdict: `Object.getPrototypeOf(${obj}) === Object.prototype && Object.keys(${obj}).length === ${count}`
563
+ } : {
564
+ statements: [`${indent}let ${KEY_COUNT_VAR} = 0;`, `${indent}for (const _k in ${obj}) ${KEY_COUNT_VAR}++;`],
565
+ verdict: `${KEY_COUNT_VAR} === ${count}`
566
+ };
551
567
  const shouldCacheVariable = (propSchema) => {
552
568
  return isSchemaObject(propSchema);
553
569
  };
554
570
  const INLINE_NESTED_MAX_PROPERTIES = 12;
555
571
  const INLINE_NESTED_MAX_SITES = 4;
556
572
  const INLINE_NESTED_MAX_TOTAL_PROPERTIES = 24;
557
- const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImports, suffix, stripUnknown, reservedNames) => {
573
+ const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImports, suffix, stripUnknown, reservedNames, claimVarName) => {
558
574
  if (!isSchemaObject(childSchema) || !hasProperties(childSchema))
559
575
  return null;
560
576
  if (!stripUnknown)
@@ -576,6 +592,7 @@ const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImpo
576
592
  const keys = Object.keys(props);
577
593
  if (keys.length === 0 || keys.length > INLINE_NESTED_MAX_PROPERTIES)
578
594
  return null;
595
+ const declarations = [];
579
596
  const checks = [];
580
597
  const fields = [];
581
598
  const readRoot = `(${objectExpr} as Record<string, any>)`;
@@ -584,22 +601,64 @@ const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImpo
584
601
  if (shouldUseRefImport(propSchema, useRefImports) || shouldUseArrayRefImport(propSchema, useRefImports) || shouldUseRecordRefImport(propSchema, useRefImports)) {
585
602
  return null;
586
603
  }
587
- const accessor = safeAccessor(readRoot, key);
604
+ const accessor = claimVarName(`${objectExpr}_${toVarName(key).slice(1)}`);
605
+ declarations.push(` const ${accessor} = ${safeAccessor(readRoot, key)};`);
588
606
  const check = generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
589
607
  if (check === null)
590
608
  return null;
591
609
  checks.push(check);
592
610
  fields.push(`${safeLiteralKey(key)}: ${accessor}`);
593
611
  }
594
- const condition = [
595
- `typeof ${objectExpr} === "object"`,
596
- `${objectExpr} !== null`,
597
- `!Array.isArray(${objectExpr})`,
598
- ...checks
599
- ].join(" && ");
600
- return { condition, literal: `{ ${fields.join(", ")} }`, propertyCount: keys.length };
612
+ const condition = checks.join(" && ");
613
+ return { declarations, condition, literal: `{ ${fields.join(", ")} }`, propertyCount: keys.length };
614
+ };
615
+ const RESULT_VARIABLE = "out";
616
+ const PROTOTYPE_MEMBERS = new Set(Object.getOwnPropertyNames(Object.prototype));
617
+ const resultMember = (key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? `${RESULT_VARIABLE}.${key}` : `${RESULT_VARIABLE}[${JSON.stringify(key)}]`;
618
+ const renderLiteralEntry = (entry, indent) => {
619
+ if (entry.kind === "field")
620
+ return `${indent}${safeLiteralKey(entry.key)}: ${entry.value},`;
621
+ if (entry.kind === "spread")
622
+ return `${indent}...${entry.expression},`;
623
+ return `${indent}...(${entry.read} !== undefined && { ${safeLiteralKey(entry.key)}: ${entry.value} }),`;
624
+ };
625
+ const renderResultObject = (entries, typeName, guard) => {
626
+ const firstOptional = entries.findIndex((entry) => entry.kind === "optional");
627
+ const assignable = entries.every((entry, index) => index < firstOptional || entry.kind === "spread" || !PROTOTYPE_MEMBERS.has(entry.key));
628
+ if (firstOptional === -1 || !assignable) {
629
+ const open = guard === void 0 ? " return {" : ` if (${guard}) return {`;
630
+ return [
631
+ open,
632
+ entries.map((entry) => renderLiteralEntry(entry, " ")).join("\n"),
633
+ ` } as unknown as ${typeName};`
634
+ ];
635
+ }
636
+ const indent = guard === void 0 ? " " : " ";
637
+ const lines = [];
638
+ if (guard !== void 0)
639
+ lines.push(` if (${guard}) {`);
640
+ const literal = entries.slice(0, firstOptional);
641
+ if (literal.length === 0) {
642
+ lines.push(`${indent}const ${RESULT_VARIABLE}: Record<string, unknown> = {};`);
643
+ } else {
644
+ lines.push(`${indent}const ${RESULT_VARIABLE}: Record<string, unknown> = {`);
645
+ lines.push(literal.map((entry) => renderLiteralEntry(entry, `${indent} `)).join("\n"));
646
+ lines.push(`${indent}};`);
647
+ }
648
+ for (const entry of entries.slice(firstOptional)) {
649
+ if (entry.kind === "field")
650
+ lines.push(`${indent}${resultMember(entry.key)} = ${entry.value};`);
651
+ else if (entry.kind === "optional") {
652
+ lines.push(`${indent}if (${entry.read} !== undefined) ${resultMember(entry.key)} = ${entry.value};`);
653
+ } else
654
+ lines.push(`${indent}Object.assign(${RESULT_VARIABLE}, ${entry.expression});`);
655
+ }
656
+ lines.push(`${indent}return ${RESULT_VARIABLE} as unknown as ${typeName};`);
657
+ if (guard !== void 0)
658
+ lines.push(" }");
659
+ return lines;
601
660
  };
602
- const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource) => {
661
+ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource, unknownKeys = DEFAULT_UNKNOWN_KEYS) => {
603
662
  const functionName = generateParserName(typeName);
604
663
  const exportPrefix = exported ? "export " : "";
605
664
  if (!hasProperties(schema)) {
@@ -618,21 +677,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
618
677
  for (const [key, subName] of subTypeNames) {
619
678
  const propSchema = schemaProps[key];
620
679
  preamble.push(`type ${subName} = NonNullable<${typeName}>[${JSON.stringify(key)}];`);
621
- const subShapeValidator = generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames);
680
+ const subShapeValidator = generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames, unknownKeys);
622
681
  preamble.push(subShapeValidator);
623
- preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, subShapeValidator));
682
+ preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, subShapeValidator, unknownKeys));
624
683
  }
625
684
  for (const [key, subName] of subItemNames) {
626
685
  const propSchema = schemaProps[key];
627
686
  const itemSchema = propSchema.items;
628
687
  preamble.push(`type ${subName} = NonNullable<NonNullable<${typeName}>[${JSON.stringify(key)}]>[number];`);
629
- const itemShapeValidator = generateShapeValidator(itemSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames);
688
+ const itemShapeValidator = generateShapeValidator(itemSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames, unknownKeys);
630
689
  preamble.push(itemShapeValidator);
631
690
  preamble.push(`const _every${subName} = (arr: readonly unknown[]): boolean => {
632
691
  for (let i = 0; i < arr.length; i++) if (!${shapeValidatorName(subName)}(arr[i])) return false;
633
692
  return true;
634
693
  };`);
635
- preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, itemShapeValidator));
694
+ preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, itemShapeValidator, unknownKeys));
636
695
  }
637
696
  const strictKeys = hasStrictKeys(schema);
638
697
  const stripKeys = strictKeys || stripUnknown;
@@ -663,7 +722,9 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
663
722
  let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema) && !(strict && inlineAllOfMembers(schema).length > 0);
664
723
  const fastPathChecks = [];
665
724
  const fastPathAccessorChecks = [];
666
- const usedVarNames = /* @__PURE__ */ new Set();
725
+ const usedVarNames = /* @__PURE__ */ new Set([KEY_COUNT_VAR]);
726
+ let inlinedGuardSites = 0;
727
+ let inlinedGuardProperties = 0;
667
728
  for (const key of propertyKeys) {
668
729
  const propSchema = schemaProps[key];
669
730
  const isRequired = isPropertyRequired(key, schema);
@@ -679,7 +740,13 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
679
740
  continue;
680
741
  }
681
742
  const subName = subTypeNames.get(key);
682
- let check = subName ? `${shapeValidatorName(subName)}(${varName})` : generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
743
+ const inlined = subName !== void 0 && inlinedGuardSites < INLINE_NESTED_MAX_SITES ? inlineNestedShapeCheck(propSchema, subName, varName, useRefImports, suffix, stripUnknown, reservedNames, unknownKeys) : null;
744
+ const withinBudget = inlined !== null && inlinedGuardProperties + inlined.propertyCount <= INLINE_NESTED_MAX_TOTAL_PROPERTIES;
745
+ if (withinBudget) {
746
+ inlinedGuardSites++;
747
+ inlinedGuardProperties += inlined.propertyCount;
748
+ }
749
+ let check = withinBudget ? inlined.check : subName ? `${shapeValidatorName(subName)}(${varName})` : generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
683
750
  const itemSubName = subItemNames.get(key);
684
751
  if (check !== null && itemSubName) {
685
752
  check = `${check} && _every${itemSubName}(${varName})`;
@@ -711,32 +778,24 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
711
778
  fastPathAccessorChecks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
712
779
  }
713
780
  }
714
- if (fastPathNeedsKnownKeys) {
715
- fastPathChecks.push(exactKeyCount !== null ? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}` : `_hasOnlyKnownKeys${typeName}(input)`);
781
+ const fastPathKeyCount = fastPathNeedsKnownKeys ? exactKeyCount : null;
782
+ if (fastPathNeedsKnownKeys && exactKeyCount === null) {
783
+ fastPathChecks.push(`_hasOnlyKnownKeys${typeName}(input)`);
716
784
  }
717
- const deepGuard = canFastPath && fastPathChecks.length > 0 ? fastPathChecks.join(" && ") : null;
785
+ const deepGuard = canFastPath && (fastPathChecks.length > 0 || fastPathKeyCount !== null) ? fastPathChecks.join(" && ") : null;
718
786
  const literalReturnUsesVars = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema)) && exported;
719
787
  let deepGuardExpr = deepGuard;
720
788
  if (deepGuard !== null && shapeValidatorSource !== void 0 && !literalReturnUsesVars && !droppedKnownKeysTerm) {
721
789
  const shapeFnName = shapeValidatorName(typeName);
722
790
  const shapeStrictKeysGuard = stripKeys && exactKeyCount === null ? `
723
791
  if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
724
- const shapeChecks = [...fastPathAccessorChecks];
725
- if (stripKeys && exactKeyCount !== null) {
726
- shapeChecks.push(`Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}`);
727
- }
728
- const expectedShapeValidator = shapeChecks.length === 0 ? shapeStrictKeysGuard ? `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => {
729
- if (!isObject(input)) return false;${shapeStrictKeysGuard}
730
- return true;
731
- };` : `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => isObject(input);` : `${exportPrefix}const ${shapeFnName} = (input: unknown): boolean => {
732
- if (!isObject(input)) return false;${shapeStrictKeysGuard}
733
- return ${shapeChecks.join("\n && ")};
734
- };`;
792
+ const expectedShapeValidator = renderShapeValidator(exportPrefix, shapeFnName, fastPathAccessorChecks, stripKeys ? exactKeyCount : null, shapeStrictKeysGuard, unknownKeys);
735
793
  if (shapeValidatorSource === expectedShapeValidator) {
736
794
  deepGuardExpr = `${shapeFnName}(input)`;
737
795
  }
738
796
  }
739
797
  const deepGuardCallsShape = deepGuardExpr !== deepGuard;
798
+ const inlineKeyCount = deepGuardCallsShape ? null : fastPathKeyCount;
740
799
  let canShallowGuard = strict && stripUnknown && !strictKeys && !hasFastPathBlockingKeyword(schema);
741
800
  const shallowChecks = [];
742
801
  if (canShallowGuard) {
@@ -776,18 +835,25 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
776
835
  warnLines.push(` }`);
777
836
  warnLines.push(` }`);
778
837
  }
779
- const buildObjectLines = (directAssign) => {
780
- const objectLines = [];
838
+ const claimNestedVarName = (base) => {
839
+ let name = base;
840
+ while (usedVarNames.has(name))
841
+ name = `${name}_`;
842
+ usedVarNames.add(name);
843
+ return name;
844
+ };
845
+ const buildObjectLines = (directAssign, hoistLines) => {
846
+ const entries = [];
781
847
  let inlinedSites = 0;
782
848
  let inlinedProperties = 0;
783
849
  if (!stripKeys) {
784
- objectLines.push(" ...input,");
850
+ entries.push({ kind: "spread", expression: "input" });
785
851
  }
786
852
  if (useRefImports && isSchemaObject(schema) && hasAllOf(schema)) {
787
853
  for (const entry of schema.allOf) {
788
854
  if (isSchemaObject(entry) && hasRef(entry)) {
789
855
  const parserName = generateParserName(refToName(entry.$ref, suffix));
790
- objectLines.push(` ...(${parserName}(input) as Record<string, unknown>),`);
856
+ entries.push({ kind: "spread", expression: `(${parserName}(input) as Record<string, unknown>)` });
791
857
  }
792
858
  }
793
859
  }
@@ -798,18 +864,23 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
798
864
  if (subName) {
799
865
  const subParserName = generateParserName(subName);
800
866
  if (isRequired) {
801
- const inlined = directAssign && !logWarnings && inlinedSites < INLINE_NESTED_MAX_SITES ? inlineNestedFastPath(propSchema, subName, accessor, useRefImports, suffix, stripUnknown, reservedNames) : null;
867
+ const inlined = hoistLines !== void 0 && directAssign && !logWarnings && inlinedSites < INLINE_NESTED_MAX_SITES ? inlineNestedFastPath(propSchema, subName, accessor, useRefImports, suffix, stripUnknown, reservedNames, claimNestedVarName) : null;
802
868
  if (inlined !== null && inlinedProperties + inlined.propertyCount <= INLINE_NESTED_MAX_TOTAL_PROPERTIES) {
803
869
  inlinedSites++;
804
870
  inlinedProperties += inlined.propertyCount;
805
- objectLines.push(` ${safeLiteralKey(key)}: (${inlined.condition})
871
+ hoistLines?.push(...inlined.declarations);
872
+ entries.push({
873
+ kind: "field",
874
+ key,
875
+ value: `(${inlined.condition})
806
876
  ? ${inlined.literal}
807
- : ${subParserName}(${accessor}),`);
877
+ : ${subParserName}(${accessor})`
878
+ });
808
879
  continue;
809
880
  }
810
- objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
881
+ entries.push({ kind: "field", key, value: `${subParserName}(${accessor})` });
811
882
  } else {
812
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
883
+ entries.push({ kind: "optional", key, read: accessor, value: `${subParserName}(${accessor})` });
813
884
  }
814
885
  continue;
815
886
  }
@@ -817,9 +888,14 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
817
888
  if (itemSubName) {
818
889
  const itemParserName = generateParserName(itemSubName);
819
890
  if (isRequired) {
820
- objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
891
+ entries.push({ kind: "field", key, value: `validateArray(${accessor}, ${itemParserName})` });
821
892
  } else {
822
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
893
+ entries.push({
894
+ kind: "optional",
895
+ key,
896
+ read: accessor,
897
+ value: `validateArray(${accessor}, ${itemParserName})`
898
+ });
823
899
  }
824
900
  continue;
825
901
  }
@@ -827,9 +903,9 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
827
903
  const ref = propSchema.$ref;
828
904
  const parserName = generateParserName(refToName(ref, suffix));
829
905
  if (isRequired) {
830
- objectLines.push(` ${safeLiteralKey(key)}: ${parserName}(${accessor}),`);
906
+ entries.push({ kind: "field", key, value: `${parserName}(${accessor})` });
831
907
  } else {
832
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${accessor}) }),`);
908
+ entries.push({ kind: "optional", key, read: accessor, value: `${parserName}(${accessor})` });
833
909
  }
834
910
  continue;
835
911
  }
@@ -838,9 +914,9 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
838
914
  const ref = items.$ref;
839
915
  const parserName = generateParserName(refToName(ref, suffix));
840
916
  if (isRequired) {
841
- objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}),`);
917
+ entries.push({ kind: "field", key, value: `validateArray(${accessor}, ${parserName})` });
842
918
  } else {
843
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
919
+ entries.push({ kind: "optional", key, read: accessor, value: `validateArray(${accessor}, ${parserName})` });
844
920
  }
845
921
  continue;
846
922
  }
@@ -849,21 +925,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
849
925
  const ref = additionalProps.$ref;
850
926
  const parserName = generateParserName(refToName(ref, suffix));
851
927
  if (isRequired) {
852
- objectLines.push(` ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}),`);
928
+ entries.push({ kind: "field", key, value: `validateRecord(${accessor}, ${parserName})` });
853
929
  } else {
854
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
930
+ entries.push({ kind: "optional", key, read: accessor, value: `validateRecord(${accessor}, ${parserName})` });
855
931
  }
856
932
  continue;
857
933
  }
858
934
  if (!isSchemaObject(propSchema)) {
859
935
  if (propSchema === true) {
860
936
  if (isRequired) {
861
- objectLines.push(` ${safeLiteralKey(key)}: ${accessor},`);
937
+ entries.push({ kind: "field", key, value: accessor });
862
938
  } else if (stripKeys) {
863
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
939
+ entries.push({ kind: "optional", key, read: accessor, value: accessor });
864
940
  }
865
941
  } else if (isRequired) {
866
- objectLines.push(` ${safeLiteralKey(key)}: undefined,`);
942
+ entries.push({ kind: "field", key, value: "undefined" });
867
943
  }
868
944
  continue;
869
945
  }
@@ -876,25 +952,47 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
876
952
  valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, void 0, void 0, shouldCache ? varName : void 0, knownNotUndefined, caseInsensitive);
877
953
  }
878
954
  if (isRequired) {
879
- objectLines.push(` ${safeLiteralKey(key)}: ${valueExpr},`);
955
+ entries.push({ kind: "field", key, value: valueExpr });
880
956
  } else {
881
- objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${valueExpr} }),`);
957
+ entries.push({ kind: "optional", key, read: accessor, value: valueExpr });
882
958
  }
883
959
  }
884
- return objectLines;
960
+ return entries;
885
961
  };
886
- const emitReturn = (lines2, objectLines) => {
887
- lines2.push(` return {`);
888
- lines2.push(objectLines.join("\n"));
889
- lines2.push(` } as unknown as ${typeName};`);
962
+ const emitReturn = (lines2, entries) => {
963
+ lines2.push(...renderResultObject(entries, typeName));
964
+ };
965
+ const withKeyCount = (lines2, guarded) => {
966
+ if (inlineKeyCount === null) {
967
+ lines2.push(...guarded(deepGuardExpr));
968
+ return;
969
+ }
970
+ const count = keyCountCheck(unknownKeys, inlineKeyCount, " ");
971
+ if (count.statements.length === 0) {
972
+ lines2.push(...guarded(deepGuardExpr === "" ? count.verdict : `${deepGuardExpr} && ${count.verdict}`));
973
+ return;
974
+ }
975
+ const counted = guarded(count.verdict);
976
+ if (deepGuardExpr === "") {
977
+ lines2.push(...keyCountCheck(unknownKeys, inlineKeyCount, " ").statements, ...counted);
978
+ return;
979
+ }
980
+ const nested = counted.map((line) => line.split("\n").map((part) => ` ${part}`).join("\n"));
981
+ lines2.push(` if (${deepGuardExpr}) {`, ...count.statements, ...nested, ` }`);
982
+ };
983
+ const emitGuardedReturn = (lines2, statement) => {
984
+ withKeyCount(lines2, (guard) => [` if (${guard}) ${statement}`]);
985
+ };
986
+ const emitGuardedResult = (lines2, fields) => {
987
+ withKeyCount(lines2, (guard) => renderResultObject(fields, typeName, guard));
890
988
  };
891
989
  const emitDeepGuardReturn = (lines2) => {
892
990
  if (!fastPathBuildsDeclaredLiteral) {
893
- lines2.push(` if (${deepGuardExpr}) return { ...input } as ${typeName};`);
991
+ emitGuardedReturn(lines2, `return { ...input } as ${typeName};`);
894
992
  return;
895
993
  }
896
994
  if (!exported && fastPathNeedsKnownKeys) {
897
- lines2.push(` if (${deepGuardExpr}) return input as ${typeName};`);
995
+ emitGuardedReturn(lines2, `return input as ${typeName};`);
898
996
  return;
899
997
  }
900
998
  const fields = [];
@@ -902,33 +1000,90 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
902
1000
  if (!isSchemaObject(propSchema)) {
903
1001
  if (propSchema === true) {
904
1002
  const acc = safeAccessor("input", key);
905
- fields.push(isRequired ? ` ${safeLiteralKey(key)}: ${acc},` : ` ...(${acc} !== undefined && { ${safeLiteralKey(key)}: ${acc} }),`);
1003
+ fields.push(isRequired ? { kind: "field", key, value: acc } : { kind: "optional", key, read: acc, value: acc });
906
1004
  } else if (isRequired) {
907
- fields.push(` ${safeLiteralKey(key)}: undefined,`);
1005
+ fields.push({ kind: "field", key, value: "undefined" });
908
1006
  }
909
1007
  continue;
910
1008
  }
911
1009
  const accessor = shouldCacheVariable(propSchema) ? varName : safeAccessor("input", key);
912
- fields.push(isRequired ? ` ${safeLiteralKey(key)}: ${accessor},` : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
1010
+ fields.push(isRequired ? { kind: "field", key, value: accessor } : { kind: "optional", key, read: accessor, value: accessor });
913
1011
  }
914
- lines2.push(` if (${deepGuardExpr}) return {`);
915
- lines2.push(fields.join("\n"));
916
- lines2.push(` } as unknown as ${typeName};`);
1012
+ emitGuardedResult(lines2, fields);
917
1013
  };
1014
+ const assertionLines = strict ? generateObjectStrictAssertion(schema, typeName, {
1015
+ useRefImports,
1016
+ suffix,
1017
+ stripUnknown,
1018
+ ...rootSchema !== void 0 ? { rootSchema } : {}
1019
+ }) : [];
1020
+ const unknownKeyThrowLines = () => rejectsUnknownKeys ? [
1021
+ ` for (const _k in input) {`,
1022
+ ` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`,
1023
+ ` }`
1024
+ ] : [];
1025
+ const varsAfterGuard = deepGuardCallsShape && shallowGuard === null;
1026
+ const assertBodyLines = [...assertionLines.slice(1), ...shallowGuard === null ? unknownKeyThrowLines() : []];
1027
+ const canSplit = !logWarnings && strict === true && (deepGuard !== null || shallowGuard !== null) && assertBodyLines.length > 0;
1028
+ const assertName = `_assert${typeName}`;
1029
+ const assertObjectName = `_assert${typeName}Object`;
1030
+ const emitAssertObjectFunction = () => `const ${assertObjectName}: (input: unknown) => asserts input is Record<string, unknown> = (input) => {
1031
+ ${assertionLines[0]}
1032
+ };`;
1033
+ const emitAssertFunction = () => `const ${assertName} = (input: Record<string, unknown>): void => {
1034
+ ${assertBodyLines.join("\n")}
1035
+ };`;
918
1036
  const lines = [];
919
1037
  lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
920
- const varsAfterGuard = deepGuardCallsShape && shallowGuard === null;
921
- if (strict) {
1038
+ if (canSplit) {
1039
+ preamble.push(emitAssertObjectFunction());
1040
+ preamble.push(emitAssertFunction());
1041
+ lines.push(` if (!isObject(input)) ${assertObjectName}(input);`);
1042
+ if (!varsAfterGuard)
1043
+ lines.push(...varDeclLines);
1044
+ if (shallowGuard) {
1045
+ lines.push(` if (!(${shallowGuard})) ${assertName}(input);`);
1046
+ if (!exported && deepGuard !== null && fastPathNeedsKnownKeys) {
1047
+ const residual = [];
1048
+ for (let i = 0; i < fastPathChecks.length; i++) {
1049
+ const deep = fastPathChecks[i];
1050
+ const shallow = shallowChecks[i];
1051
+ if (deep === shallow)
1052
+ continue;
1053
+ residual.push(shallow !== void 0 && deep.startsWith(`${shallow} && `) ? deep.slice(shallow.length + 4) : deep);
1054
+ }
1055
+ const returnInput = `return input as ${typeName};`;
1056
+ if (fastPathKeyCount === null) {
1057
+ lines.push(residual.length > 0 ? ` if (${residual.join(" && ")}) ${returnInput}` : ` ${returnInput}`);
1058
+ } else {
1059
+ const count = keyCountCheck(unknownKeys, fastPathKeyCount, residual.length === 0 ? " " : " ");
1060
+ if (count.statements.length === 0) {
1061
+ lines.push(` if (${[...residual, count.verdict].join(" && ")}) ${returnInput}`);
1062
+ } else if (residual.length === 0) {
1063
+ lines.push(...count.statements, ` if (${count.verdict}) ${returnInput}`);
1064
+ } else {
1065
+ lines.push(` if (${residual.join(" && ")}) {`);
1066
+ lines.push(...count.statements, ` if (${count.verdict}) ${returnInput}`);
1067
+ lines.push(` }`);
1068
+ }
1069
+ }
1070
+ }
1071
+ const hoisted = [];
1072
+ const objectLines = buildObjectLines(true, hoisted);
1073
+ lines.push(...hoisted);
1074
+ emitReturn(lines, objectLines);
1075
+ } else {
1076
+ emitDeepGuardReturn(lines);
1077
+ if (varsAfterGuard)
1078
+ lines.push(...varDeclLines);
1079
+ lines.push(` ${assertName}(input);`);
1080
+ emitReturn(lines, buildObjectLines(true));
1081
+ }
1082
+ } else if (strict) {
922
1083
  lines.push(` if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`);
923
1084
  if (!varsAfterGuard)
924
1085
  lines.push(...varDeclLines);
925
1086
  lines.push(...warnLines);
926
- const assertionLines = generateObjectStrictAssertion(schema, typeName, {
927
- useRefImports,
928
- suffix,
929
- stripUnknown,
930
- ...rootSchema !== void 0 ? { rootSchema } : {}
931
- }).slice(1);
932
1087
  if (shallowGuard) {
933
1088
  if (!exported && deepGuard && fastPathNeedsKnownKeys) {
934
1089
  const residual = [];
@@ -940,34 +1095,41 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
940
1095
  residual.push(shallow !== void 0 && deep.startsWith(`${shallow} && `) ? deep.slice(shallow.length + 4) : deep);
941
1096
  }
942
1097
  lines.push(` if (${shallowGuard}) {`);
943
- lines.push(residual.length > 0 ? ` if (${residual.join(" && ")}) return input as ${typeName};` : ` return input as ${typeName};`);
1098
+ const returnInput = `return input as ${typeName};`;
1099
+ if (fastPathKeyCount === null) {
1100
+ lines.push(residual.length > 0 ? ` if (${residual.join(" && ")}) ${returnInput}` : ` ${returnInput}`);
1101
+ } else {
1102
+ const count = keyCountCheck(unknownKeys, fastPathKeyCount, residual.length === 0 ? " " : " ");
1103
+ if (count.statements.length === 0) {
1104
+ lines.push(` if (${[...residual, count.verdict].join(" && ")}) ${returnInput}`);
1105
+ } else if (residual.length === 0) {
1106
+ lines.push(...count.statements, ` if (${count.verdict}) ${returnInput}`);
1107
+ } else {
1108
+ lines.push(` if (${residual.join(" && ")}) {`);
1109
+ lines.push(...count.statements, ` if (${count.verdict}) ${returnInput}`);
1110
+ lines.push(` }`);
1111
+ }
1112
+ }
944
1113
  lines.push(` } else {`);
945
- for (const assertionLine of assertionLines) {
1114
+ for (const assertionLine of assertionLines.slice(1))
946
1115
  lines.push(` ${assertionLine}`);
947
- }
948
1116
  lines.push(` }`);
949
1117
  } else {
950
1118
  lines.push(` if (!(${shallowGuard})) {`);
951
- for (const assertionLine of assertionLines) {
1119
+ for (const assertionLine of assertionLines.slice(1))
952
1120
  lines.push(` ${assertionLine}`);
953
- }
954
1121
  lines.push(` }`);
955
1122
  }
956
1123
  emitReturn(lines, buildObjectLines(true));
957
1124
  } else {
958
- if (deepGuard) {
1125
+ if (deepGuard !== null) {
959
1126
  emitDeepGuardReturn(lines);
960
1127
  }
961
1128
  if (varsAfterGuard)
962
1129
  lines.push(...varDeclLines);
963
- for (const assertionLine of assertionLines) {
1130
+ for (const assertionLine of assertionLines.slice(1))
964
1131
  lines.push(assertionLine);
965
- }
966
- if (rejectsUnknownKeys) {
967
- lines.push(` for (const _k in input) {`);
968
- lines.push(` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
969
- lines.push(` }`);
970
- }
1132
+ lines.push(...unknownKeyThrowLines());
971
1133
  emitReturn(lines, buildObjectLines(true));
972
1134
  }
973
1135
  } else {
@@ -975,7 +1137,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
975
1137
  if (!varsAfterGuard)
976
1138
  lines.push(...varDeclLines);
977
1139
  lines.push(...warnLines);
978
- if (deepGuard) {
1140
+ if (deepGuard !== null) {
979
1141
  emitDeepGuardReturn(lines);
980
1142
  }
981
1143
  if (varsAfterGuard)
@@ -1034,6 +1196,7 @@ ${loopLines.join("\n")}
1034
1196
  };
1035
1197
  const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false, caseInsensitive = false, context = {}) => {
1036
1198
  const functionName = generateParserName(typeName);
1199
+ const unknownKeys = context.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
1037
1200
  const entries = generatePropertyEntries(schema, useRefImports, suffix, caseInsensitive);
1038
1201
  const propertyLines = entries.map((entry) => {
1039
1202
  if (entry.isOptional) {
@@ -1042,7 +1205,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1042
1205
  return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
1043
1206
  });
1044
1207
  if (!isSchemaObject(schema) || !("patternProperties" in schema)) {
1045
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
1208
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
1046
1209
  }
1047
1210
  const patternProps = schema.patternProperties;
1048
1211
  const patterns = Object.entries(patternProps);
@@ -1055,7 +1218,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1055
1218
  return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, false, context);
1056
1219
  }
1057
1220
  if (!refPattern || !useRefImports) {
1058
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
1221
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
1059
1222
  }
1060
1223
  const [pattern, patternSchema] = refPattern;
1061
1224
  const ref = patternSchema.$ref;
@@ -1231,7 +1394,7 @@ ${notObjectBranch}
1231
1394
  return result;
1232
1395
  };`;
1233
1396
  };
1234
- const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false, caseInsensitive = false) => {
1397
+ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false, caseInsensitive = false, unknownKeys = DEFAULT_UNKNOWN_KEYS) => {
1235
1398
  const functionName = generateParserName(typeName);
1236
1399
  const ifSchema = schema.if;
1237
1400
  const thenSchema = schema.then;
@@ -1241,7 +1404,7 @@ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = fals
1241
1404
  if (!thenHasRef || !elseHasRef) {
1242
1405
  const mergedSchema = getConditionalObjectSchema(schema);
1243
1406
  if (mergedSchema) {
1244
- return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
1407
+ return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
1245
1408
  }
1246
1409
  return generateEmptyObjectParser(typeName);
1247
1410
  }
@@ -1249,7 +1412,7 @@ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = fals
1249
1412
  if (!isRefCondition) {
1250
1413
  const mergedSchema = getConditionalObjectSchema(schema);
1251
1414
  if (mergedSchema) {
1252
- return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
1415
+ return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
1253
1416
  }
1254
1417
  return generateEmptyObjectParser(typeName);
1255
1418
  }
@@ -1333,11 +1496,13 @@ const selectParserStrategy = (schema, typeName, options) => {
1333
1496
  const strict = options?.strict ?? false;
1334
1497
  const stripUnknown = options?.stripUnknown ?? false;
1335
1498
  const caseInsensitive = options?.caseInsensitive ?? false;
1499
+ const unknownKeys = options?.unknownKeys ?? DEFAULT_UNKNOWN_KEYS;
1336
1500
  const suffix = options?.typeSuffix ?? "";
1337
1501
  const context = {
1338
1502
  useRefImports,
1339
1503
  suffix,
1340
1504
  stripUnknown,
1505
+ unknownKeys,
1341
1506
  ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {}
1342
1507
  };
1343
1508
  const copyLoopContext = { ...context, subParsers: false };
@@ -1352,6 +1517,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1352
1517
  stripUnknown,
1353
1518
  caseInsensitive,
1354
1519
  logWarnings,
1520
+ unknownKeys,
1355
1521
  ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1356
1522
  ...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
1357
1523
  });
@@ -1366,12 +1532,13 @@ const selectParserStrategy = (schema, typeName, options) => {
1366
1532
  stripUnknown,
1367
1533
  caseInsensitive,
1368
1534
  logWarnings,
1535
+ unknownKeys,
1369
1536
  ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1370
1537
  ...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
1371
1538
  });
1372
1539
  }
1373
1540
  if (hasProperties(schema)) {
1374
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive, options?.shapeValidatorSource);
1541
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive, options?.shapeValidatorSource, unknownKeys);
1375
1542
  }
1376
1543
  if (strict && isSchemaObject(schema) && ("if" in schema || "then" in schema || "else" in schema)) {
1377
1544
  return generateNonObjectParser(typeName, schema, strict, {
@@ -1380,16 +1547,17 @@ const selectParserStrategy = (schema, typeName, options) => {
1380
1547
  stripUnknown,
1381
1548
  caseInsensitive,
1382
1549
  logWarnings,
1550
+ unknownKeys,
1383
1551
  ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1384
1552
  ...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
1385
1553
  });
1386
1554
  }
1387
1555
  if (isSchemaObject(schema) && "if" in schema && "then" in schema && "else" in schema) {
1388
- return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive);
1556
+ return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive, unknownKeys);
1389
1557
  }
1390
1558
  const conditionalObjectSchema = getConditionalObjectSchema(schema);
1391
1559
  if (conditionalObjectSchema) {
1392
- return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
1560
+ return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive, void 0, unknownKeys);
1393
1561
  }
1394
1562
  if (!isObjectLikeSchema) {
1395
1563
  return generateNonObjectParser(typeName, schema, strict, {
@@ -1398,6 +1566,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1398
1566
  stripUnknown,
1399
1567
  caseInsensitive,
1400
1568
  logWarnings,
1569
+ unknownKeys,
1401
1570
  ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1402
1571
  ...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
1403
1572
  });
@@ -1430,7 +1599,7 @@ const generateParserFunction = (schema, typeName, rawOptions) => {
1430
1599
  return selectParserStrategy(schema, typeName, options);
1431
1600
  };
1432
1601
  const shapeValidatorFunctionName = (typeName) => shapeValidatorName(typeName);
1433
- const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", exported = true, stripUnknown = false, reservedNames = NO_RESERVED_NAMES) => {
1602
+ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", exported = true, stripUnknown = false, reservedNames = NO_RESERVED_NAMES, unknownKeys = DEFAULT_UNKNOWN_KEYS) => {
1434
1603
  const fnName = shapeValidatorName(typeName);
1435
1604
  const exportPrefix = exported ? "export " : "";
1436
1605
  const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
@@ -1455,25 +1624,34 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
1455
1624
  }
1456
1625
  return stub;
1457
1626
  }
1458
- if (!hasProperties(schema))
1627
+ const shape = shapeChecksOf(schema, typeName, "input", useRefImports, suffix, stripUnknown, reservedNames);
1628
+ if (shape === null)
1459
1629
  return stub;
1630
+ const strictKeysGuard = shape.walksKeys ? `
1631
+ if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
1632
+ return renderShapeValidator(exportPrefix, fnName, shape.checks, shape.keyCount, strictKeysGuard, unknownKeys);
1633
+ };
1634
+ const shapeChecksOf = (schema, typeName, root, useRefImports, suffix, stripUnknown, reservedNames) => {
1635
+ if (!isSchemaObject(schema))
1636
+ return null;
1637
+ if (hasFastPathBlockingKeyword(schema))
1638
+ return null;
1639
+ if (!hasProperties(schema))
1640
+ return null;
1460
1641
  if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema || "patternProperties" in schema || "if" in schema || "then" in schema || "else" in schema) {
1461
- return stub;
1642
+ return null;
1462
1643
  }
1463
1644
  const schemaProps = schema.properties;
1464
1645
  const propertyKeys = Object.keys(schemaProps);
1465
1646
  const { objects: subTypeNames, arrayItems: subItemNames } = collectInlineSubTypes(schema, typeName, reservedNames);
1466
1647
  const validatorStripKeys = hasStrictKeys(schema) || stripUnknown;
1467
- const validatorKeyCount = validatorStripKeys ? exactKeyCountOf(schema) : null;
1468
- const strictKeysGuard = validatorStripKeys && validatorKeyCount === null ? `
1469
- if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
1648
+ const keyCount = validatorStripKeys ? exactKeyCountOf(schema) : null;
1470
1649
  const checks = [];
1471
1650
  for (const key of propertyKeys) {
1472
1651
  const propSchema = schemaProps[key];
1473
- if (shouldUseRecordRefImport(propSchema, useRefImports)) {
1474
- return stub;
1475
- }
1476
- const accessor = safeAccessor("input", key);
1652
+ if (shouldUseRecordRefImport(propSchema, useRefImports))
1653
+ return null;
1654
+ const accessor = safeAccessor(root, key);
1477
1655
  const isRequired = isPropertyRequired(key, schema);
1478
1656
  const subName = subTypeNames.get(key);
1479
1657
  let check = subName ? `${shapeValidatorName(subName)}(${accessor})` : generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
@@ -1481,9 +1659,8 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
1481
1659
  if (check !== null && itemSubName) {
1482
1660
  check = `${check} && _every${itemSubName}(${accessor})`;
1483
1661
  }
1484
- if (check === null) {
1485
- return stub;
1486
- }
1662
+ if (check === null)
1663
+ return null;
1487
1664
  if (isRequired) {
1488
1665
  checks.push(check);
1489
1666
  } else {
@@ -1491,28 +1668,56 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
1491
1668
  }
1492
1669
  }
1493
1670
  if (hasMinProperties(schema))
1494
- checks.push(`Object.keys(input).length >= ${schema.minProperties}`);
1671
+ checks.push(`Object.keys(${root}).length >= ${schema.minProperties}`);
1495
1672
  if (hasMaxProperties(schema))
1496
- checks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
1497
- if (validatorKeyCount !== null) {
1498
- checks.push(`Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${validatorKeyCount}`);
1499
- }
1500
- if (checks.length === 0) {
1501
- if (strictKeysGuard) {
1502
- return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1503
- if (!isObject(input)) return false;${strictKeysGuard}
1673
+ checks.push(`Object.keys(${root}).length <= ${schema.maxProperties}`);
1674
+ return { checks, keyCount, walksKeys: validatorStripKeys && keyCount === null, propertyCount: propertyKeys.length };
1675
+ };
1676
+ const inlineNestedShapeCheck = (childSchema, childTypeName, local, useRefImports, suffix, stripUnknown, reservedNames, unknownKeys) => {
1677
+ const readRoot = `(${local} as Record<string, any>)`;
1678
+ const shape = shapeChecksOf(childSchema, childTypeName, readRoot, useRefImports, suffix, stripUnknown, reservedNames);
1679
+ if (shape === null || shape.walksKeys)
1680
+ return null;
1681
+ if (shape.propertyCount === 0 || shape.propertyCount > INLINE_NESTED_MAX_PROPERTIES)
1682
+ return null;
1683
+ const count = shape.keyCount === null ? null : keyCountCheck(unknownKeys, shape.keyCount, "", local);
1684
+ if (count !== null && count.statements.length > 0)
1685
+ return null;
1686
+ const parts = [
1687
+ `typeof ${local} === "object"`,
1688
+ `${local} !== null`,
1689
+ `!Array.isArray(${local})`,
1690
+ ...shape.checks,
1691
+ ...count === null ? [] : [count.verdict]
1692
+ ];
1693
+ return { check: `(${parts.join(" && ")})`, propertyCount: shape.propertyCount };
1694
+ };
1695
+ const renderShapeValidator = (exportPrefix, fnName, checks, keyCount, strictKeysGuard, unknownKeys) => {
1696
+ const head = `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1697
+ if (!isObject(input)) return false;${strictKeysGuard}`;
1698
+ const chain = checks.join("\n && ");
1699
+ if (keyCount === null) {
1700
+ if (checks.length === 0) {
1701
+ return strictKeysGuard ? `${head}
1504
1702
  return true;
1505
- };`;
1703
+ };` : `${exportPrefix}const ${fnName} = (input: unknown): boolean => isObject(input);`;
1506
1704
  }
1507
- return `${exportPrefix}const ${fnName} = (input: unknown): boolean => isObject(input);`;
1705
+ return `${head}
1706
+ return ${chain};
1707
+ };`;
1508
1708
  }
1509
- let body = checks[0];
1510
- for (let i = 1; i < checks.length; i++) {
1511
- body += "\n && " + checks[i];
1709
+ const count = keyCountCheck(unknownKeys, keyCount, " ");
1710
+ if (count.statements.length === 0) {
1711
+ return `${head}
1712
+ return ${checks.length === 0 ? count.verdict : `${chain}
1713
+ && ${count.verdict}`};
1714
+ };`;
1512
1715
  }
1513
- return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1514
- if (!isObject(input)) return false;${strictKeysGuard}
1515
- return ${body};
1716
+ const gate = checks.length === 0 ? "" : `
1717
+ if (!(${chain})) return false;`;
1718
+ return `${head}${gate}
1719
+ ${count.statements.join("\n")}
1720
+ return ${count.verdict};
1516
1721
  };`;
1517
1722
  };
1518
1723
  export {
@@ -1,3 +1,4 @@
1
+ import type { UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
1
2
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
3
  /**
3
4
  * Context for assertions that reach beyond the property's own schema: union
@@ -13,6 +14,11 @@ export type StrictAssertionContext = {
13
14
  readonly suffix?: string;
14
15
  readonly rootSchema?: Record<string, unknown>;
15
16
  readonly stripUnknown?: boolean;
17
+ /**
18
+ * The parser's `unknownKeys` option, carried so every object parser one of
19
+ * these contexts reaches counts keys on its fast path the same way.
20
+ */
21
+ readonly unknownKeys?: UnknownKeysStrategy;
16
22
  /**
17
23
  * Whether the parser these assertions run inside also emits the private
18
24
  * sub-parsers for its inline object properties (and inline-object array
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -41,7 +41,11 @@
41
41
  "types:check": "tsgo -p . --noEmit",
42
42
  "test": "NODE_ENV=production vitest run --root ../.. generate-parsers",
43
43
  "generate-readme": "bun run ./scripts/generate-readme.ts",
44
- "bench": "bun --conditions development ./bench/run.ts"
44
+ "prebench": "bun run --filter='@amritk/helpers' build && bun run build",
45
+ "bench": "bun --conditions development ./bench/run.ts",
46
+ "prebench:node": "bun run --filter='@amritk/helpers' build && bun run build",
47
+ "bench:node": "node ./bench/run.ts",
48
+ "bench:moltar": "bun --conditions development ./bench/moltar.ts"
45
49
  },
46
50
  "imports": {
47
51
  "#generators/*": "./src/generators/*.ts",
@@ -56,12 +60,13 @@
56
60
  },
57
61
  "dependencies": {
58
62
  "json-schema-typed": "^8.0.1",
59
- "@amritk/helpers": "^0.18.0"
63
+ "@amritk/helpers": "^0.19.0"
60
64
  },
61
65
  "devDependencies": {
62
- "@amritk/generate-markdown": "^0.5.0",
66
+ "@amritk/generate-markdown": "^0.6.0",
63
67
  "@sinclair/typebox": "^0.34.49",
64
68
  "ajv": "^8.17.1",
69
+ "benny": "^3.7.1",
65
70
  "zod": "^4.4.3"
66
71
  }
67
72
  }