@amritk/generate-parsers 0.15.0 → 0.16.1

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.
@@ -8,9 +8,11 @@ import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
8
8
  import { findDiscriminator } from '../helpers/find-discriminator.js';
9
9
  import { getDefaultValue } from '../helpers/get-default-value.js';
10
10
  import { getDiscriminatorValue } from '../helpers/get-discriminator-value.js';
11
- import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
12
- import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName, } from './generate-type-checks.js';
13
- import { generateValidationExpression, isCoercibleItemSchema } from './generate-validation-expression.js';
11
+ import { assertNoUnsupportedKeywords } from './assert-supported-keywords.js';
12
+ import { generateEnumCaseInsensitiveCoercion } from './generate-enum-check.js';
13
+ import { generateContainsCheck, generateObjectKeywordChecks, generateObjectStrictAssertion, generateScalarStrictAssertion, } from './generate-strict-assertion.js';
14
+ import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName, } from './generate-type-checks.js';
15
+ import { generatePrefixItemsMap, generateValidationExpression, getPrefixItems, isCoercibleItemSchema, prefixItemsCapsLength, } from './generate-validation-expression.js';
14
16
  /**
15
17
  * Generates the parser function name from a type name.
16
18
  * Converts "UserObject" to "parseUserObject".
@@ -18,6 +20,16 @@ import { generateValidationExpression, isCoercibleItemSchema } from './generate-
18
20
  const generateParserName = (typeName) => {
19
21
  return `parse${typeName}`;
20
22
  };
23
+ /**
24
+ * Object-literal key form that is safe for the runtime-dangerous name
25
+ * `__proto__`. In an object literal `{ __proto__: v }` / `{ "__proto__": v }` is
26
+ * the special prototype-setter syntax — it never creates an own property — so a
27
+ * schema property literally named `__proto__` would silently vanish (and, for an
28
+ * object value, reassign the result's prototype). The computed form
29
+ * `{ ["__proto__"]: v }` creates a normal own property. Every other key keeps
30
+ * the plain `safeKey` output, so the common path is unchanged.
31
+ */
32
+ const safeLiteralKey = (key) => (key === '__proto__' ? '["__proto__"]' : safeKey(key));
21
33
  /**
22
34
  * Checks if a property is required based on the schema's required array.
23
35
  */
@@ -49,7 +61,7 @@ const generateOptionalRefCall = (key, ref, suffix) => {
49
61
  // Gate on presence, not truthiness: a `&&` guard skips parsing when the value
50
62
  // is `false`/`0`/`""`/`null`, spreading it raw. `!== undefined` still omits an
51
63
  // absent optional property but coerces every present one, matching the main path.
52
- return `...(${acc} !== undefined && { ${safeKey(key)}: ${parserName}(${acc}) })`;
64
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${acc}) })`;
53
65
  };
54
66
  /**
55
67
  * Generates a validateArray call for required array properties with $ref items.
@@ -67,7 +79,7 @@ const generateOptionalArrayRefCall = (key, ref, suffix) => {
67
79
  const parserName = generateParserName(refToName(ref, suffix));
68
80
  const acc = safeAccessor('input', key);
69
81
  // Presence gating (not truthiness) so a falsy-but-present value is still parsed.
70
- return `...(${acc} !== undefined && { ${safeKey(key)}: validateArray(${acc}, ${parserName}) })`;
82
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateArray(${acc}, ${parserName}) })`;
71
83
  };
72
84
  /**
73
85
  * Generates a validateRecord call for required object properties with additionalProperties $ref.
@@ -86,14 +98,14 @@ const generateOptionalRecordRefCall = (key, ref, suffix) => {
86
98
  const acc = safeAccessor('input', key);
87
99
  const parserName = generateParserName(refToName(ref, suffix));
88
100
  // Presence gating (not truthiness) so a falsy-but-present value is still parsed.
89
- return `...(${acc} !== undefined && { ${safeKey(key)}: validateRecord(${acc}, ${parserName}) })`;
101
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${acc}, ${parserName}) })`;
90
102
  };
91
103
  /**
92
104
  * Generates a spread entry for optional inline properties.
93
105
  * The value expression is evaluated once and omitted when undefined.
94
106
  */
95
107
  const generateOptionalInlineProperty = (key, valueExpression) => {
96
- return `...((value => value === undefined ? {} : { ${safeKey(key)}: value })(${valueExpression}))`;
108
+ return `...((value => value === undefined ? {} : { ${safeLiteralKey(key)}: value })(${valueExpression}))`;
97
109
  };
98
110
  /**
99
111
  * Determines if a property schema should use ref imports for validation.
@@ -147,7 +159,7 @@ const shouldUseRecordRefImport = (propSchema, useRefImports) => {
147
159
  /**
148
160
  * Generates the value expression for a property based on its schema and options.
149
161
  */
150
- const generatePropertyValue = (key, propSchema, isRequired, useRefImports, suffix) => {
162
+ const generatePropertyValue = (key, propSchema, isRequired, useRefImports, suffix, caseInsensitive = false) => {
151
163
  // Handle direct $ref properties
152
164
  if (shouldUseRefImport(propSchema, useRefImports)) {
153
165
  const ref = propSchema.$ref;
@@ -173,20 +185,20 @@ const generatePropertyValue = (key, propSchema, isRequired, useRefImports, suffi
173
185
  }
174
186
  // Generate standard validation expression
175
187
  const defaultValue = getDefaultValue(propSchema);
176
- const valueExpression = generateValidationExpression(key, propSchema, defaultValue, isRequired);
188
+ const valueExpression = generateValidationExpression(key, propSchema, defaultValue, isRequired, undefined, undefined, undefined, undefined, caseInsensitive);
177
189
  return isRequired ? valueExpression : generateOptionalInlineProperty(key, valueExpression);
178
190
  };
179
191
  /**
180
192
  * Generates property entries for all properties in the schema.
181
193
  */
182
- const generatePropertyEntries = (schema, useRefImports, suffix) => {
194
+ const generatePropertyEntries = (schema, useRefImports, suffix, caseInsensitive = false) => {
183
195
  if (!hasProperties(schema)) {
184
196
  return [];
185
197
  }
186
198
  const entries = [];
187
199
  for (const [key, propSchema] of Object.entries(schema.properties)) {
188
200
  const isRequired = isPropertyRequired(key, schema);
189
- const value = generatePropertyValue(key, propSchema, isRequired, useRefImports, suffix);
201
+ const value = generatePropertyValue(key, propSchema, isRequired, useRefImports, suffix, caseInsensitive);
190
202
  // Determine if the property uses spread syntax (making it optional in the object literal)
191
203
  const isOptional = value.startsWith('...');
192
204
  entries.push({ key, value, isOptional });
@@ -255,7 +267,7 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
255
267
  if (fallbackValue === 'undefined') {
256
268
  return `{} as ${typeName}`;
257
269
  }
258
- requiredProps.push(` ${safeKey(key)}: ${fallbackValue},`);
270
+ requiredProps.push(` ${safeLiteralKey(key)}: ${fallbackValue},`);
259
271
  }
260
272
  }
261
273
  if (requiredProps.length === 0) {
@@ -419,7 +431,11 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
419
431
  if (hasEnum(schema) && schema.enum.length > 0) {
420
432
  const values = JSON.stringify(schema.enum);
421
433
  const fallback = JSON.stringify(schema.enum[0]);
422
- return `export const ${functionName} = (input: unknown): ${typeName} => ${values}.includes(input as never) ? input as ${typeName} : ${fallback} as ${typeName};`;
434
+ // Case-insensitive normalization sits on the non-member branch only, so an
435
+ // exact member still returns via the `includes` fast path untouched.
436
+ const ci = unionCtx?.caseInsensitive ? generateEnumCaseInsensitiveCoercion('input', schema.enum, fallback) : null;
437
+ const coerced = ci ? `(${ci})` : fallback;
438
+ return `export const ${functionName} = (input: unknown): ${typeName} => ${values}.includes(input as never) ? input as ${typeName} : ${coerced} as ${typeName};`;
423
439
  }
424
440
  // A top-level union must validate membership: an unmatched value is not of
425
441
  // the declared union type, so coerce it to a member-shaped default. Reuse the
@@ -451,6 +467,10 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
451
467
  !Array.isArray(schema.items)) {
452
468
  const items = schema.items;
453
469
  const notArrayThrow = `if (!Array.isArray(input)) throw new Error(\`[${typeName}] expected array, got \${input === null ? "null" : typeof input}\`);`;
470
+ // This delegating path bypasses generateScalarStrictAssertion, so enforce the
471
+ // array-level `contains` constraint here too — otherwise a root array of
472
+ // objects/$refs would silently ignore it in strict mode.
473
+ const strictArrayChecks = strict ? generateContainsCheck('input', schema, `[${typeName}]`).join('\n') : '';
454
474
  // validateArray identity-returns the input array when every element parses
455
475
  // to itself; this parser is EXPORTED, and exported parsers never alias the
456
476
  // value the caller passed in (matching the scalar root-array path's
@@ -458,7 +478,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
458
478
  // element references are still shared, like every other fast path.
459
479
  const delegatedBody = (itemParserName) => ` const _parsed = validateArray(input, ${itemParserName});\n return (_parsed === input ? [..._parsed] : _parsed) as ${typeName};`;
460
480
  const delegated = (itemParserName) => strict
461
- ? `export const ${functionName} = (input: unknown): ${typeName} => {\n ${notArrayThrow}\n${delegatedBody(itemParserName)}\n};`
481
+ ? `export const ${functionName} = (input: unknown): ${typeName} => {\n ${notArrayThrow}${strictArrayChecks ? `\n${strictArrayChecks}` : ''}\n${delegatedBody(itemParserName)}\n};`
462
482
  : `export const ${functionName} = (input: unknown): ${typeName} => {\n${delegatedBody(itemParserName)}\n};`;
463
483
  if (unionCtx?.useRefImports && isSchemaObject(items) && hasRef(items)) {
464
484
  return delegated(generateParserName(refToName(items.$ref, unionCtx.suffix)));
@@ -477,7 +497,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
477
497
  const preamble = [
478
498
  `type ${itemName} = ${typeName}[number];`,
479
499
  generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved),
480
- generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved),
500
+ generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false),
481
501
  ].join('\n\n');
482
502
  return `${preamble}\n\n${delegated(generateParserName(itemName))}`;
483
503
  }
@@ -490,7 +510,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
490
510
  return `export const ${functionName} = (input: unknown): ${typeName} => null as ${typeName};`;
491
511
  }
492
512
  if (strict) {
493
- const assertion = generateScalarStrictAssertion(schema, typeName);
513
+ const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
494
514
  if (assertion === null) {
495
515
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
496
516
  }
@@ -508,10 +528,17 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
508
528
  case 'boolean':
509
529
  return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
510
530
  case 'array': {
531
+ // Tuple `prefixItems`: coerce each declared position and, under items:false,
532
+ // drop elements past the tuple length. A non-array coerces to an empty array.
533
+ const prefixItems = getPrefixItems(schema);
534
+ if (prefixItems) {
535
+ const mapped = generatePrefixItemsMap('input', prefixItems, prefixItemsCapsLength(schema), unionCtx?.rootSchema, undefined, unionCtx?.caseInsensitive);
536
+ return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? ${mapped} as ${typeName} : [] as ${typeName};`;
537
+ }
511
538
  // Coerce each element when the item schema is a single scalar type or an enum.
512
539
  if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
513
540
  const item = schema.items;
514
- const itemExpr = generateValidationExpression('', item, getDefaultValue(item), true, undefined, undefined, '_it', true);
541
+ const itemExpr = generateValidationExpression('', item, getDefaultValue(item), true, undefined, undefined, '_it', true, unionCtx?.caseInsensitive);
515
542
  return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? (input as unknown[]).map((_it) => ${itemExpr}) as ${typeName} : [] as ${typeName};`;
516
543
  }
517
544
  return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? [...input] as ${typeName} : [] as ${typeName};`;
@@ -520,15 +547,33 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
520
547
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
521
548
  }
522
549
  };
550
+ /**
551
+ * Object-level keyword checks (`propertyNames` / `dependentSchemas` /
552
+ * `dependentRequired`) rendered as a newline-prefixed prelude for the
553
+ * specialized strict object parsers (empty-object, pattern-properties,
554
+ * additionalProperties, combined) that build their result outside
555
+ * generateObjectStrictAssertion. Each of those emits its own
556
+ * `if (!isObject(input)) throw` first, so the checks run against a proven object
557
+ * (`guard = false`). Returns `''` when the schema carries none of the keywords,
558
+ * so existing output for every other schema is byte-for-byte unchanged.
559
+ */
560
+ const strictObjectKeywordPrelude = (schema, typeName) => {
561
+ if (!isSchemaObject(schema))
562
+ return '';
563
+ const lines = generateObjectKeywordChecks('input', schema, `[${typeName}]`, false);
564
+ return lines.length > 0 ? `\n${lines.join('\n')}` : '';
565
+ };
523
566
  /**
524
567
  * Generates a parser for empty object schemas or schemas with only additionalProperties.
525
568
  * Validates the input is an object before casting, falling back to an empty object.
526
- * Returns a shallow copy to avoid mutating the original input.
569
+ * Returns a shallow copy to avoid mutating the original input. `schema`, when
570
+ * given, supplies any object-level keyword checks the strict parser must enforce.
527
571
  */
528
- const generateEmptyObjectParser = (typeName, strict) => {
572
+ const generateEmptyObjectParser = (typeName, strict, schema) => {
529
573
  const functionName = generateParserName(typeName);
530
574
  if (strict) {
531
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return { ...input } as ${typeName};\n};`;
575
+ const prelude = schema !== undefined ? strictObjectKeywordPrelude(schema, typeName) : '';
576
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${prelude}\n return { ...input } as ${typeName};\n};`;
532
577
  }
533
578
  return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {} as ${typeName};`;
534
579
  };
@@ -664,12 +709,12 @@ const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
664
709
  * - Fast path that returns input directly when all properties are already valid
665
710
  * - Slow path with optimized expressions
666
711
  */
667
- const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES) => {
712
+ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false) => {
668
713
  const functionName = generateParserName(typeName);
669
714
  const exportPrefix = exported ? 'export ' : '';
670
715
  if (!hasProperties(schema)) {
671
716
  if (strict) {
672
- return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return input as ${typeName};\n};`;
717
+ return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${strictObjectKeywordPrelude(schema, typeName)}\n return input as ${typeName};\n};`;
673
718
  }
674
719
  return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
675
720
  }
@@ -690,7 +735,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
690
735
  const propSchema = schemaProps[key];
691
736
  preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
692
737
  preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames));
693
- preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames));
738
+ preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive));
694
739
  }
695
740
  for (const [key, subName] of subItemNames) {
696
741
  const propSchema = schemaProps[key];
@@ -701,7 +746,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
701
746
  // Hand-rolled loop instead of Array.prototype.every on the guard path — the
702
747
  // callback protocol costs a few percent on element-heavy hot paths.
703
748
  preamble.push(`const _every${subName} = (arr: readonly unknown[]): boolean => {\n for (let i = 0; i < arr.length; i++) if (!${shapeValidatorName(subName)}(arr[i])) return false;\n return true;\n};`);
704
- preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames));
749
+ preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive));
705
750
  }
706
751
  // additionalProperties: false — the per-key "is this undeclared" test inlines
707
752
  // `!==` comparisons for a short key list and hoists a Set only for a long one
@@ -745,7 +790,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
745
790
  isSchemaObject(schema) &&
746
791
  hasAllOf(schema) &&
747
792
  schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
748
- let canFastPath = !hasAllOfRefParsers;
793
+ // Object-level keywords the fast path cannot mirror (dependentRequired,
794
+ // dependentSchemas, propertyNames) force the slow path so the strict assertion
795
+ // actually runs them; returning `{ ...input }` on a bare shape match would skip
796
+ // the enforcement. (A property-level blocker like `contains` is already caught
797
+ // per-property by generatePropertyTypeCheck returning null.)
798
+ let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema);
749
799
  const fastPathChecks = [];
750
800
  // `toVarName` collapses distinct keys that differ only in non-identifier chars
751
801
  // (e.g. `a-b` and `a.b` both → `_a_b`), which would emit two `const _a_b`
@@ -814,7 +864,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
814
864
  // stripUnknown input — which carries extras the build removes, and nested
815
865
  // extras each sub-parser removes — while the cold path's per-property
816
866
  // assertions still produce the precise error on a genuine mismatch.
817
- let canShallowGuard = strict && stripUnknown && !strictKeys;
867
+ let canShallowGuard = strict && stripUnknown && !strictKeys && !hasFastPathBlockingKeyword(schema);
818
868
  const shallowChecks = [];
819
869
  if (canShallowGuard) {
820
870
  for (const { key, varName, isRequired, propSchema } of propInfo) {
@@ -901,10 +951,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
901
951
  if (subName) {
902
952
  const subParserName = generateParserName(subName);
903
953
  if (isRequired) {
904
- objectLines.push(` ${safeKey(key)}: ${subParserName}(${accessor}),`);
954
+ objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
905
955
  }
906
956
  else {
907
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${subParserName}(${accessor}) }),`);
957
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
908
958
  }
909
959
  continue;
910
960
  }
@@ -916,10 +966,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
916
966
  if (itemSubName) {
917
967
  const itemParserName = generateParserName(itemSubName);
918
968
  if (isRequired) {
919
- objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
969
+ objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
920
970
  }
921
971
  else {
922
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
972
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
923
973
  }
924
974
  continue;
925
975
  }
@@ -928,10 +978,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
928
978
  const ref = propSchema.$ref;
929
979
  const parserName = generateParserName(refToName(ref, suffix));
930
980
  if (isRequired) {
931
- objectLines.push(` ${safeKey(key)}: ${parserName}(${accessor}),`);
981
+ objectLines.push(` ${safeLiteralKey(key)}: ${parserName}(${accessor}),`);
932
982
  }
933
983
  else {
934
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${parserName}(${accessor}) }),`);
984
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${accessor}) }),`);
935
985
  }
936
986
  continue;
937
987
  }
@@ -941,10 +991,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
941
991
  const ref = items.$ref;
942
992
  const parserName = generateParserName(refToName(ref, suffix));
943
993
  if (isRequired) {
944
- objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${parserName}),`);
994
+ objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}),`);
945
995
  }
946
996
  else {
947
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
997
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
948
998
  }
949
999
  continue;
950
1000
  }
@@ -954,17 +1004,17 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
954
1004
  const ref = additionalProps.$ref;
955
1005
  const parserName = generateParserName(refToName(ref, suffix));
956
1006
  if (isRequired) {
957
- objectLines.push(` ${safeKey(key)}: validateRecord(${accessor}, ${parserName}),`);
1007
+ objectLines.push(` ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}),`);
958
1008
  }
959
1009
  else {
960
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
1010
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
961
1011
  }
962
1012
  continue;
963
1013
  }
964
1014
  // Handle non-schema-object properties
965
1015
  if (!isSchemaObject(propSchema)) {
966
1016
  if (isRequired) {
967
- objectLines.push(` ${safeKey(key)}: undefined,`);
1017
+ objectLines.push(` ${safeLiteralKey(key)}: undefined,`);
968
1018
  }
969
1019
  continue;
970
1020
  }
@@ -979,13 +1029,13 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
979
1029
  // For optional properties we know the value is not undefined because
980
1030
  // we're inside the `...(accessor !== undefined && { ... })` check.
981
1031
  const knownNotUndefined = !isRequired;
982
- valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined);
1032
+ valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined, caseInsensitive);
983
1033
  }
984
1034
  if (isRequired) {
985
- objectLines.push(` ${safeKey(key)}: ${valueExpr},`);
1035
+ objectLines.push(` ${safeLiteralKey(key)}: ${valueExpr},`);
986
1036
  }
987
1037
  else {
988
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${valueExpr} }),`);
1038
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${valueExpr} }),`);
989
1039
  }
990
1040
  }
991
1041
  return objectLines;
@@ -1027,15 +1077,15 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
1027
1077
  for (const { key, varName, isRequired, propSchema } of propInfo) {
1028
1078
  if (!isSchemaObject(propSchema)) {
1029
1079
  if (isRequired)
1030
- fields.push(` ${safeKey(key)}: undefined,`);
1080
+ fields.push(` ${safeLiteralKey(key)}: undefined,`);
1031
1081
  continue;
1032
1082
  }
1033
1083
  const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports)
1034
1084
  ? varName
1035
1085
  : safeAccessor('input', key);
1036
1086
  fields.push(isRequired
1037
- ? ` ${safeKey(key)}: ${accessor},`
1038
- : ` ...(${accessor} !== undefined && { ${safeKey(key)}: ${accessor} }),`);
1087
+ ? ` ${safeLiteralKey(key)}: ${accessor},`
1088
+ : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
1039
1089
  }
1040
1090
  lines.push(` if (${deepGuard}) return {`);
1041
1091
  lines.push(fields.join('\n'));
@@ -1143,6 +1193,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
1143
1193
  * its imported parser when ref imports are on), and every other key is rejected
1144
1194
  * in strict mode or stripped in coerce mode — matching the interpreter.
1145
1195
  */
1196
+ /**
1197
+ * Emits a prototype-safe `result[key] = value` for the dynamic `for..in` copy
1198
+ * loops in the pattern-property parsers. A bare `result[key] = v` is a `[[Set]]`
1199
+ * — and for the key `"__proto__"` (own-enumerable when the input came from
1200
+ * `JSON.parse`) it invokes the inherited `Object.prototype` setter and reassigns
1201
+ * `result`'s prototype to attacker-controlled data, the same vector
1202
+ * `validateRecord` guards against. `"__proto__"` is the only own-enumerable key
1203
+ * whose name resolves to an accessor on `Object.prototype` (`constructor` et al.
1204
+ * are writable data properties, so a `[[Set]]` there just creates an own key),
1205
+ * so a single-keyed guard is sufficient. Each loop already runs a `.test(key)`
1206
+ * regex per iteration, which dominates the cost, so the extra `===` on the
1207
+ * common path is immeasurable. Assumes the target variable is named `result`,
1208
+ * as it is in every parser that calls this.
1209
+ */
1210
+ const safeResultAssign = (keyVar, valueExpr) => `if (${keyVar} === "__proto__") { Object.defineProperty(result, ${keyVar}, { value: ${valueExpr}, writable: true, enumerable: true, configurable: true }); } else { (result as Record<string, unknown>)[${keyVar}] = ${valueExpr}; }`;
1146
1211
  const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict) => {
1147
1212
  const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
1148
1213
  // Below the inline threshold a key === "a" || key === "b" chain skips declared
@@ -1155,7 +1220,7 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1155
1220
  if (refPattern) {
1156
1221
  const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
1157
1222
  loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
1158
- loopLines.push(` (result as Record<string, unknown>)[key] = ${parserName}(input[key]);`);
1223
+ loopLines.push(` ${safeResultAssign('key', `${parserName}(input[key])`)}`);
1159
1224
  loopLines.push(` continue;`);
1160
1225
  loopLines.push(` }`);
1161
1226
  }
@@ -1164,7 +1229,7 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1164
1229
  .map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1165
1230
  if (keepConditions.length > 0) {
1166
1231
  loopLines.push(` if (${keepConditions.join(' || ')}) {`);
1167
- loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
1232
+ loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
1168
1233
  loopLines.push(` continue;`);
1169
1234
  loopLines.push(` }`);
1170
1235
  }
@@ -1178,10 +1243,11 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1178
1243
  const resultBody = propertyLines.length > 0 ? `{\n${propertyLines.join('\n')}\n }` : '{}';
1179
1244
  // Only the Set form needs a declaration; the inline === chain is stateless.
1180
1245
  const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};\n`).join('');
1246
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1181
1247
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1182
1248
  if (!isObject(input)) {
1183
1249
  ${notObjectBranch}
1184
- }
1250
+ }${keywordPrelude}
1185
1251
  ${knownKeysDeclaration} const result = ${resultBody} as unknown as ${typeName};
1186
1252
  for (const key in input) {
1187
1253
  ${loopLines.join('\n')}
@@ -1193,19 +1259,19 @@ ${loopLines.join('\n')}
1193
1259
  * Generates a parser for schemas that have both properties AND patternProperties.
1194
1260
  * Parses the known properties first, then iterates remaining keys to match patterns.
1195
1261
  */
1196
- const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false) => {
1262
+ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false, caseInsensitive = false) => {
1197
1263
  const functionName = generateParserName(typeName);
1198
- const entries = generatePropertyEntries(schema, useRefImports, suffix);
1264
+ const entries = generatePropertyEntries(schema, useRefImports, suffix, caseInsensitive);
1199
1265
  // Build the known property lines for the initial object
1200
1266
  const propertyLines = entries.map((entry) => {
1201
1267
  if (entry.isOptional) {
1202
1268
  return ` ${entry.value},`;
1203
1269
  }
1204
- return ` ${safeKey(entry.key)}: ${entry.value},`;
1270
+ return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
1205
1271
  });
1206
1272
  // Find the first pattern with a $ref for parser delegation
1207
1273
  if (!isSchemaObject(schema) || !('patternProperties' in schema)) {
1208
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
1274
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1209
1275
  }
1210
1276
  const patternProps = schema.patternProperties;
1211
1277
  const patterns = Object.entries(patternProps);
@@ -1219,7 +1285,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1219
1285
  return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict);
1220
1286
  }
1221
1287
  if (!refPattern || !useRefImports) {
1222
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
1288
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1223
1289
  }
1224
1290
  const [pattern, patternSchema] = refPattern;
1225
1291
  const ref = patternSchema.$ref;
@@ -1236,10 +1302,11 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1236
1302
  const notObjectBranch = strict
1237
1303
  ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1238
1304
  : ` return {} as unknown as ${typeName};`;
1305
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1239
1306
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1240
1307
  if (!isObject(input)) {
1241
1308
  ${notObjectBranch}
1242
- }
1309
+ }${keywordPrelude}
1243
1310
  const result = {
1244
1311
  ${objectProperties}
1245
1312
  } as unknown as ${typeName};
@@ -1260,14 +1327,15 @@ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suf
1260
1327
  const additionalProps = schema.additionalProperties;
1261
1328
  // Check if additionalProps is defined before using it
1262
1329
  if (!additionalProps) {
1263
- return generateEmptyObjectParser(typeName, strict);
1330
+ return generateEmptyObjectParser(typeName, strict, schema);
1264
1331
  }
1332
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1265
1333
  // If additionalProperties is a $ref and useRefImports is true, generate a loop
1266
1334
  if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
1267
1335
  const ref = additionalProps.$ref;
1268
1336
  const parserName = generateParserName(refToName(ref, suffix));
1269
1337
  if (strict) {
1270
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return validateRecord(input, ${parserName}) as ${typeName};\n};`;
1338
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return validateRecord(input, ${parserName}) as ${typeName};\n};`;
1271
1339
  }
1272
1340
  return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${parserName}) as ${typeName};`;
1273
1341
  }
@@ -1276,14 +1344,14 @@ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suf
1276
1344
  const inlineParser = generateInlineValueParser(additionalProps);
1277
1345
  if (inlineParser) {
1278
1346
  if (strict) {
1279
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return validateRecord(input, ${inlineParser}) as ${typeName};\n};`;
1347
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return validateRecord(input, ${inlineParser}) as ${typeName};\n};`;
1280
1348
  }
1281
1349
  return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${inlineParser}) as ${typeName};`;
1282
1350
  }
1283
1351
  }
1284
1352
  // Otherwise, just validate the input type and shallow copy
1285
1353
  if (strict) {
1286
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return { ...input } as ${typeName};\n};`;
1354
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${keywordPrelude}\n return { ...input } as ${typeName};\n};`;
1287
1355
  }
1288
1356
  return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {};`;
1289
1357
  };
@@ -1302,8 +1370,11 @@ const generateInlineValueParser = (schema) => {
1302
1370
  case 'string':
1303
1371
  return '(value: unknown) => typeof value === "string" ? value : ""';
1304
1372
  case 'number':
1305
- case 'integer':
1306
1373
  return '(value: unknown) => typeof value === "number" ? value : 0';
1374
+ case 'integer':
1375
+ // `integer` rejects non-integral numbers; a bare typeof accepts `1.5`,
1376
+ // matching every other integer site in this package (and strict mode).
1377
+ return '(value: unknown) => typeof value === "number" && Number.isInteger(value) ? value : 0';
1307
1378
  case 'boolean':
1308
1379
  return '(value: unknown) => typeof value === "boolean" ? value : false';
1309
1380
  case 'array':
@@ -1319,47 +1390,53 @@ const generateInlineValueParser = (schema) => {
1319
1390
  const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
1320
1391
  const functionName = generateParserName(typeName);
1321
1392
  if (!('patternProperties' in schema) || typeof schema.patternProperties !== 'object') {
1322
- return generateEmptyObjectParser(typeName, strict);
1393
+ return generateEmptyObjectParser(typeName, strict, schema);
1323
1394
  }
1324
1395
  const patternProps = schema.patternProperties;
1325
1396
  // Find the first pattern and its schema
1326
1397
  const patterns = Object.entries(patternProps);
1327
1398
  if (patterns.length === 0) {
1328
- return generateEmptyObjectParser(typeName, strict);
1399
+ return generateEmptyObjectParser(typeName, strict, schema);
1329
1400
  }
1330
1401
  const [pattern, patternSchema] = patterns[0];
1331
1402
  let patternAssignment = '(result as Record<string, unknown>)[key] = value;';
1403
+ // The value-expression form, used by the strictKeys path (which builds from
1404
+ // `{}`) to emit a prototype-safe assignment. `null` when no value is allowed.
1405
+ let patternValueExpr = 'value';
1332
1406
  // Use imported parser when pattern schema points to a $ref.
1333
1407
  if (useRefImports && isSchemaObject(patternSchema) && hasRef(patternSchema)) {
1334
1408
  const ref = patternSchema.$ref;
1335
1409
  const parserName = generateParserName(refToName(ref, suffix));
1336
1410
  patternAssignment = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
1411
+ patternValueExpr = `${parserName}(value)`;
1337
1412
  }
1338
1413
  else if (patternSchema === false) {
1339
1414
  // `false` means no values are allowed for matching keys.
1340
1415
  patternAssignment = '';
1416
+ patternValueExpr = null;
1341
1417
  }
1342
1418
  // Escape the pattern for safe inclusion in generated code
1343
1419
  const escapedPattern = escapeRegexPattern(pattern);
1344
1420
  const notObjectBranch = strict
1345
1421
  ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1346
1422
  : ` return {} as unknown as ${typeName};`;
1423
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1347
1424
  // With `additionalProperties: false`, only keys matching a pattern survive:
1348
1425
  // others are rejected (strict) or stripped (coerce). Start from an empty
1349
1426
  // object rather than spreading every input key.
1350
1427
  const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
1351
1428
  if (strictKeys) {
1352
1429
  const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
1353
- if (patternAssignment) {
1430
+ if (patternValueExpr) {
1354
1431
  loopLines.push(` const value = input[key];`);
1355
- loopLines.push(` ${patternAssignment}`);
1432
+ loopLines.push(` ${safeResultAssign('key', patternValueExpr)}`);
1356
1433
  }
1357
1434
  loopLines.push(` continue;`);
1358
1435
  loopLines.push(` }`);
1359
1436
  const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1360
1437
  if (keepConditions.length > 0) {
1361
1438
  loopLines.push(` if (${keepConditions.join(' || ')}) {`);
1362
- loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
1439
+ loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
1363
1440
  loopLines.push(` continue;`);
1364
1441
  loopLines.push(` }`);
1365
1442
  }
@@ -1368,7 +1445,7 @@ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix
1368
1445
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1369
1446
  if (!isObject(input)) {
1370
1447
  ${notObjectBranch}
1371
- }
1448
+ }${keywordPrelude}
1372
1449
  const result = {} as unknown as ${typeName};
1373
1450
  for (const key in input) {
1374
1451
  ${loopLines.join('\n')}
@@ -1380,7 +1457,7 @@ ${loopLines.join('\n')}
1380
1457
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1381
1458
  if (!isObject(input)) {
1382
1459
  ${notObjectBranch}
1383
- }
1460
+ }${keywordPrelude}
1384
1461
  const result = {
1385
1462
  ...input,
1386
1463
  } as unknown as ${typeName};
@@ -1398,7 +1475,7 @@ ${notObjectBranch}
1398
1475
  * This always generates parser calls regardless of useRefImports setting,
1399
1476
  * because conditional logic requires delegating to the appropriate parser.
1400
1477
  */
1401
- const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false) => {
1478
+ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false, caseInsensitive = false) => {
1402
1479
  const functionName = generateParserName(typeName);
1403
1480
  // Extract the condition and branches
1404
1481
  const ifSchema = schema.if;
@@ -1413,7 +1490,7 @@ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = fals
1413
1490
  // regular object parser from the merged property set.
1414
1491
  const mergedSchema = getConditionalObjectSchema(schema);
1415
1492
  if (mergedSchema) {
1416
- return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown);
1493
+ return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1417
1494
  }
1418
1495
  return generateEmptyObjectParser(typeName);
1419
1496
  }
@@ -1427,7 +1504,7 @@ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = fals
1427
1504
  // Condition is not a $ref check — flatten into an object parser
1428
1505
  const mergedSchema = getConditionalObjectSchema(schema);
1429
1506
  if (mergedSchema) {
1430
- return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown);
1507
+ return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1431
1508
  }
1432
1509
  return generateEmptyObjectParser(typeName);
1433
1510
  }
@@ -1523,6 +1600,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1523
1600
  const logWarnings = options?.logWarnings ?? false;
1524
1601
  const strict = options?.strict ?? false;
1525
1602
  const stripUnknown = options?.stripUnknown ?? false;
1603
+ const caseInsensitive = options?.caseInsensitive ?? false;
1526
1604
  const suffix = options?.typeSuffix ?? '';
1527
1605
  // Special case for the self-referential JSON Schema meta-schema type (e.g.
1528
1606
  // `Schema` / `SchemaObject`) - it can be any JSON Schema. This is an OpenAPI
@@ -1541,6 +1619,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1541
1619
  useRefImports,
1542
1620
  suffix,
1543
1621
  stripUnknown,
1622
+ caseInsensitive,
1544
1623
  logWarnings,
1545
1624
  ...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
1546
1625
  ...(options?.reservedNames !== undefined ? { reservedNames: options.reservedNames } : {}),
@@ -1550,24 +1629,24 @@ const selectParserStrategy = (schema, typeName, options) => {
1550
1629
  // This generates a parser that handles known properties and also iterates
1551
1630
  // pattern-matched keys (e.g. responses with "default" + "200", "4XX").
1552
1631
  if (hasProperties(schema) && isSchemaObject(schema) && 'patternProperties' in schema) {
1553
- return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown);
1632
+ return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive);
1554
1633
  }
1555
1634
  // Handle schemas that have explicit properties — generate a full object parser.
1556
1635
  // This intentionally runs before if/then checks so that schemas with both
1557
1636
  // properties AND conditional keywords use all declared properties rather than
1558
1637
  // only the if/then fragment.
1559
1638
  if (hasProperties(schema)) {
1560
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames);
1639
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive);
1561
1640
  }
1562
1641
  // Handle conditional schemas (if/then/else) for schemas without explicit properties.
1563
1642
  if (isSchemaObject(schema) && 'if' in schema && 'then' in schema && 'else' in schema) {
1564
- return generateConditionalParser(schema, typeName, suffix, stripUnknown);
1643
+ return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive);
1565
1644
  }
1566
1645
  // Handle conditional schemas that only define if/then object fragments.
1567
1646
  // We flatten the fragments into a regular object parser.
1568
1647
  const conditionalObjectSchema = getConditionalObjectSchema(schema);
1569
1648
  if (conditionalObjectSchema) {
1570
- return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
1649
+ return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1571
1650
  }
1572
1651
  // Handle non-object schemas with type-appropriate validation (no properties, no conditionals)
1573
1652
  if (!isObjectLikeSchema) {
@@ -1575,6 +1654,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1575
1654
  useRefImports,
1576
1655
  suffix,
1577
1656
  stripUnknown,
1657
+ caseInsensitive,
1578
1658
  logWarnings,
1579
1659
  ...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
1580
1660
  ...(options?.reservedNames !== undefined ? { reservedNames: options.reservedNames } : {}),
@@ -1589,17 +1669,17 @@ const selectParserStrategy = (schema, typeName, options) => {
1589
1669
  const additionalProps = schema.additionalProperties;
1590
1670
  // If additionalProperties is true or false, validate it is an object
1591
1671
  if (additionalProps === true || additionalProps === false) {
1592
- return generateEmptyObjectParser(typeName, strict);
1672
+ return generateEmptyObjectParser(typeName, strict, schema);
1593
1673
  }
1594
1674
  // Otherwise, handle as a schema (could be a $ref or object schema)
1595
1675
  return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1596
1676
  }
1597
1677
  // Handle empty object schemas
1598
1678
  if ('type' in schema && schema.type === 'object' && !hasProperties(schema)) {
1599
- return generateEmptyObjectParser(typeName, strict);
1679
+ return generateEmptyObjectParser(typeName, strict, schema);
1600
1680
  }
1601
1681
  // Default fallback - validate it is an object since we passed the isObjectSchema check
1602
- return generateEmptyObjectParser(typeName, strict);
1682
+ return generateEmptyObjectParser(typeName, strict, schema);
1603
1683
  };
1604
1684
  /**
1605
1685
  * Generates a safe parser as an arrow function that never throws.
@@ -1611,6 +1691,12 @@ const selectParserStrategy = (schema, typeName, options) => {
1611
1691
  * $ref will map each element through the imported parser.
1612
1692
  */
1613
1693
  export const generateParserFunction = (schema, typeName, options) => {
1694
+ // Strict parsers promise to throw on violations, so any keyword we cannot
1695
+ // enforce must fail loudly at generation rather than emit a permissive parser.
1696
+ // Coercing parsers are documented to repair rather than reject, so the guard
1697
+ // does not apply to them.
1698
+ if (options?.strict)
1699
+ assertNoUnsupportedKeywords(schema, typeName);
1614
1700
  return selectParserStrategy(schema, typeName, options);
1615
1701
  };
1616
1702
  /**
@@ -1637,6 +1723,12 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
1637
1723
  const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
1638
1724
  if (!isSchemaObject(schema))
1639
1725
  return stub;
1726
+ // Keywords the parser's fast path cannot mirror (contains, dependentRequired,
1727
+ // dependentSchemas, propertyNames) make a bare shape match insufficient: a
1728
+ // parent that fast-pathed through this predicate would skip their enforcement,
1729
+ // so stub out and force the parent's slow path (which calls the real parser).
1730
+ if (hasFastPathBlockingKeyword(schema))
1731
+ return stub;
1640
1732
  // An alias definition (a bare `$ref` with no shape of its own) delegates to
1641
1733
  // the referenced type's validator. Guarded against self-reference.
1642
1734
  if (useRefImports && hasRef(schema) && !hasProperties(schema)) {