@amritk/generate-parsers 0.16.0 → 0.16.2

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,10 +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 { assertNoUnsupportedKeywords } from './assert-supported-keywords.js';
11
12
  import { generateEnumCaseInsensitiveCoercion } from './generate-enum-check.js';
12
- import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
13
- import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName, } from './generate-type-checks.js';
14
- import { generateValidationExpression, isCoercibleItemSchema } from './generate-validation-expression.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';
15
16
  /**
16
17
  * Generates the parser function name from a type name.
17
18
  * Converts "UserObject" to "parseUserObject".
@@ -19,6 +20,16 @@ import { generateValidationExpression, isCoercibleItemSchema } from './generate-
19
20
  const generateParserName = (typeName) => {
20
21
  return `parse${typeName}`;
21
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));
22
33
  /**
23
34
  * Checks if a property is required based on the schema's required array.
24
35
  */
@@ -50,7 +61,7 @@ const generateOptionalRefCall = (key, ref, suffix) => {
50
61
  // Gate on presence, not truthiness: a `&&` guard skips parsing when the value
51
62
  // is `false`/`0`/`""`/`null`, spreading it raw. `!== undefined` still omits an
52
63
  // absent optional property but coerces every present one, matching the main path.
53
- return `...(${acc} !== undefined && { ${safeKey(key)}: ${parserName}(${acc}) })`;
64
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${acc}) })`;
54
65
  };
55
66
  /**
56
67
  * Generates a validateArray call for required array properties with $ref items.
@@ -68,7 +79,7 @@ const generateOptionalArrayRefCall = (key, ref, suffix) => {
68
79
  const parserName = generateParserName(refToName(ref, suffix));
69
80
  const acc = safeAccessor('input', key);
70
81
  // Presence gating (not truthiness) so a falsy-but-present value is still parsed.
71
- return `...(${acc} !== undefined && { ${safeKey(key)}: validateArray(${acc}, ${parserName}) })`;
82
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateArray(${acc}, ${parserName}) })`;
72
83
  };
73
84
  /**
74
85
  * Generates a validateRecord call for required object properties with additionalProperties $ref.
@@ -87,14 +98,14 @@ const generateOptionalRecordRefCall = (key, ref, suffix) => {
87
98
  const acc = safeAccessor('input', key);
88
99
  const parserName = generateParserName(refToName(ref, suffix));
89
100
  // Presence gating (not truthiness) so a falsy-but-present value is still parsed.
90
- return `...(${acc} !== undefined && { ${safeKey(key)}: validateRecord(${acc}, ${parserName}) })`;
101
+ return `...(${acc} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${acc}, ${parserName}) })`;
91
102
  };
92
103
  /**
93
104
  * Generates a spread entry for optional inline properties.
94
105
  * The value expression is evaluated once and omitted when undefined.
95
106
  */
96
107
  const generateOptionalInlineProperty = (key, valueExpression) => {
97
- return `...((value => value === undefined ? {} : { ${safeKey(key)}: value })(${valueExpression}))`;
108
+ return `...((value => value === undefined ? {} : { ${safeLiteralKey(key)}: value })(${valueExpression}))`;
98
109
  };
99
110
  /**
100
111
  * Determines if a property schema should use ref imports for validation.
@@ -256,7 +267,7 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
256
267
  if (fallbackValue === 'undefined') {
257
268
  return `{} as ${typeName}`;
258
269
  }
259
- requiredProps.push(` ${safeKey(key)}: ${fallbackValue},`);
270
+ requiredProps.push(` ${safeLiteralKey(key)}: ${fallbackValue},`);
260
271
  }
261
272
  }
262
273
  if (requiredProps.length === 0) {
@@ -443,6 +454,18 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
443
454
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
444
455
  }
445
456
  }
457
+ // Strict counterpart of the block above for a *type-less* `const`/`enum` root
458
+ // (`{ enum: [...] }` / `{ const: ... }` with no `type`): the no-`type` bail
459
+ // below would otherwise return a bare cast, so strict mode has to assert
460
+ // membership here. A typed `const`/`enum` (`{ type: 'string', enum: [...] }`)
461
+ // keeps flowing to the strict scalar path, which asserts it via
462
+ // generateScalarStrictAssertion.
463
+ if (strict && isSchemaObject(schema) && !hasType(schema) && (hasConst(schema) || hasEnum(schema))) {
464
+ const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
465
+ if (assertion !== null) {
466
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n${assertion}\n return input as ${typeName};\n};`;
467
+ }
468
+ }
446
469
  // Root-level arrays with rich item schemas delegate every element to a real
447
470
  // parser, so nested enums and $refs inside array items are validated instead
448
471
  // of spread through unchecked. $ref items call the imported parser; inline
@@ -456,6 +479,10 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
456
479
  !Array.isArray(schema.items)) {
457
480
  const items = schema.items;
458
481
  const notArrayThrow = `if (!Array.isArray(input)) throw new Error(\`[${typeName}] expected array, got \${input === null ? "null" : typeof input}\`);`;
482
+ // This delegating path bypasses generateScalarStrictAssertion, so enforce the
483
+ // array-level `contains` constraint here too — otherwise a root array of
484
+ // objects/$refs would silently ignore it in strict mode.
485
+ const strictArrayChecks = strict ? generateContainsCheck('input', schema, `[${typeName}]`).join('\n') : '';
459
486
  // validateArray identity-returns the input array when every element parses
460
487
  // to itself; this parser is EXPORTED, and exported parsers never alias the
461
488
  // value the caller passed in (matching the scalar root-array path's
@@ -463,7 +490,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
463
490
  // element references are still shared, like every other fast path.
464
491
  const delegatedBody = (itemParserName) => ` const _parsed = validateArray(input, ${itemParserName});\n return (_parsed === input ? [..._parsed] : _parsed) as ${typeName};`;
465
492
  const delegated = (itemParserName) => strict
466
- ? `export const ${functionName} = (input: unknown): ${typeName} => {\n ${notArrayThrow}\n${delegatedBody(itemParserName)}\n};`
493
+ ? `export const ${functionName} = (input: unknown): ${typeName} => {\n ${notArrayThrow}${strictArrayChecks ? `\n${strictArrayChecks}` : ''}\n${delegatedBody(itemParserName)}\n};`
467
494
  : `export const ${functionName} = (input: unknown): ${typeName} => {\n${delegatedBody(itemParserName)}\n};`;
468
495
  if (unionCtx?.useRefImports && isSchemaObject(items) && hasRef(items)) {
469
496
  return delegated(generateParserName(refToName(items.$ref, unionCtx.suffix)));
@@ -495,7 +522,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
495
522
  return `export const ${functionName} = (input: unknown): ${typeName} => null as ${typeName};`;
496
523
  }
497
524
  if (strict) {
498
- const assertion = generateScalarStrictAssertion(schema, typeName);
525
+ const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
499
526
  if (assertion === null) {
500
527
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
501
528
  }
@@ -513,6 +540,13 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
513
540
  case 'boolean':
514
541
  return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
515
542
  case 'array': {
543
+ // Tuple `prefixItems`: coerce each declared position and, under items:false,
544
+ // drop elements past the tuple length. A non-array coerces to an empty array.
545
+ const prefixItems = getPrefixItems(schema);
546
+ if (prefixItems) {
547
+ const mapped = generatePrefixItemsMap('input', prefixItems, prefixItemsCapsLength(schema), unionCtx?.rootSchema, undefined, unionCtx?.caseInsensitive);
548
+ return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? ${mapped} as ${typeName} : [] as ${typeName};`;
549
+ }
516
550
  // Coerce each element when the item schema is a single scalar type or an enum.
517
551
  if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
518
552
  const item = schema.items;
@@ -525,15 +559,33 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
525
559
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
526
560
  }
527
561
  };
562
+ /**
563
+ * Object-level keyword checks (`propertyNames` / `dependentSchemas` /
564
+ * `dependentRequired`) rendered as a newline-prefixed prelude for the
565
+ * specialized strict object parsers (empty-object, pattern-properties,
566
+ * additionalProperties, combined) that build their result outside
567
+ * generateObjectStrictAssertion. Each of those emits its own
568
+ * `if (!isObject(input)) throw` first, so the checks run against a proven object
569
+ * (`guard = false`). Returns `''` when the schema carries none of the keywords,
570
+ * so existing output for every other schema is byte-for-byte unchanged.
571
+ */
572
+ const strictObjectKeywordPrelude = (schema, typeName) => {
573
+ if (!isSchemaObject(schema))
574
+ return '';
575
+ const lines = generateObjectKeywordChecks('input', schema, `[${typeName}]`, false);
576
+ return lines.length > 0 ? `\n${lines.join('\n')}` : '';
577
+ };
528
578
  /**
529
579
  * Generates a parser for empty object schemas or schemas with only additionalProperties.
530
580
  * Validates the input is an object before casting, falling back to an empty object.
531
- * Returns a shallow copy to avoid mutating the original input.
581
+ * Returns a shallow copy to avoid mutating the original input. `schema`, when
582
+ * given, supplies any object-level keyword checks the strict parser must enforce.
532
583
  */
533
- const generateEmptyObjectParser = (typeName, strict) => {
584
+ const generateEmptyObjectParser = (typeName, strict, schema) => {
534
585
  const functionName = generateParserName(typeName);
535
586
  if (strict) {
536
- 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};`;
587
+ const prelude = schema !== undefined ? strictObjectKeywordPrelude(schema, typeName) : '';
588
+ 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};`;
537
589
  }
538
590
  return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {} as ${typeName};`;
539
591
  };
@@ -674,7 +726,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
674
726
  const exportPrefix = exported ? 'export ' : '';
675
727
  if (!hasProperties(schema)) {
676
728
  if (strict) {
677
- 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};`;
729
+ 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};`;
678
730
  }
679
731
  return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
680
732
  }
@@ -750,7 +802,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
750
802
  isSchemaObject(schema) &&
751
803
  hasAllOf(schema) &&
752
804
  schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
753
- let canFastPath = !hasAllOfRefParsers;
805
+ // Object-level keywords the fast path cannot mirror (dependentRequired,
806
+ // dependentSchemas, propertyNames) force the slow path so the strict assertion
807
+ // actually runs them; returning `{ ...input }` on a bare shape match would skip
808
+ // the enforcement. (A property-level blocker like `contains` is already caught
809
+ // per-property by generatePropertyTypeCheck returning null.)
810
+ let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema);
754
811
  const fastPathChecks = [];
755
812
  // `toVarName` collapses distinct keys that differ only in non-identifier chars
756
813
  // (e.g. `a-b` and `a.b` both → `_a_b`), which would emit two `const _a_b`
@@ -819,7 +876,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
819
876
  // stripUnknown input — which carries extras the build removes, and nested
820
877
  // extras each sub-parser removes — while the cold path's per-property
821
878
  // assertions still produce the precise error on a genuine mismatch.
822
- let canShallowGuard = strict && stripUnknown && !strictKeys;
879
+ let canShallowGuard = strict && stripUnknown && !strictKeys && !hasFastPathBlockingKeyword(schema);
823
880
  const shallowChecks = [];
824
881
  if (canShallowGuard) {
825
882
  for (const { key, varName, isRequired, propSchema } of propInfo) {
@@ -906,10 +963,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
906
963
  if (subName) {
907
964
  const subParserName = generateParserName(subName);
908
965
  if (isRequired) {
909
- objectLines.push(` ${safeKey(key)}: ${subParserName}(${accessor}),`);
966
+ objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
910
967
  }
911
968
  else {
912
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${subParserName}(${accessor}) }),`);
969
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
913
970
  }
914
971
  continue;
915
972
  }
@@ -921,10 +978,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
921
978
  if (itemSubName) {
922
979
  const itemParserName = generateParserName(itemSubName);
923
980
  if (isRequired) {
924
- objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
981
+ objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
925
982
  }
926
983
  else {
927
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
984
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
928
985
  }
929
986
  continue;
930
987
  }
@@ -933,10 +990,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
933
990
  const ref = propSchema.$ref;
934
991
  const parserName = generateParserName(refToName(ref, suffix));
935
992
  if (isRequired) {
936
- objectLines.push(` ${safeKey(key)}: ${parserName}(${accessor}),`);
993
+ objectLines.push(` ${safeLiteralKey(key)}: ${parserName}(${accessor}),`);
937
994
  }
938
995
  else {
939
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${parserName}(${accessor}) }),`);
996
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${accessor}) }),`);
940
997
  }
941
998
  continue;
942
999
  }
@@ -946,10 +1003,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
946
1003
  const ref = items.$ref;
947
1004
  const parserName = generateParserName(refToName(ref, suffix));
948
1005
  if (isRequired) {
949
- objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${parserName}),`);
1006
+ objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}),`);
950
1007
  }
951
1008
  else {
952
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
1009
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
953
1010
  }
954
1011
  continue;
955
1012
  }
@@ -959,17 +1016,17 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
959
1016
  const ref = additionalProps.$ref;
960
1017
  const parserName = generateParserName(refToName(ref, suffix));
961
1018
  if (isRequired) {
962
- objectLines.push(` ${safeKey(key)}: validateRecord(${accessor}, ${parserName}),`);
1019
+ objectLines.push(` ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}),`);
963
1020
  }
964
1021
  else {
965
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
1022
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
966
1023
  }
967
1024
  continue;
968
1025
  }
969
1026
  // Handle non-schema-object properties
970
1027
  if (!isSchemaObject(propSchema)) {
971
1028
  if (isRequired) {
972
- objectLines.push(` ${safeKey(key)}: undefined,`);
1029
+ objectLines.push(` ${safeLiteralKey(key)}: undefined,`);
973
1030
  }
974
1031
  continue;
975
1032
  }
@@ -987,10 +1044,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
987
1044
  valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined, caseInsensitive);
988
1045
  }
989
1046
  if (isRequired) {
990
- objectLines.push(` ${safeKey(key)}: ${valueExpr},`);
1047
+ objectLines.push(` ${safeLiteralKey(key)}: ${valueExpr},`);
991
1048
  }
992
1049
  else {
993
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${valueExpr} }),`);
1050
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${valueExpr} }),`);
994
1051
  }
995
1052
  }
996
1053
  return objectLines;
@@ -1032,15 +1089,15 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
1032
1089
  for (const { key, varName, isRequired, propSchema } of propInfo) {
1033
1090
  if (!isSchemaObject(propSchema)) {
1034
1091
  if (isRequired)
1035
- fields.push(` ${safeKey(key)}: undefined,`);
1092
+ fields.push(` ${safeLiteralKey(key)}: undefined,`);
1036
1093
  continue;
1037
1094
  }
1038
1095
  const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports)
1039
1096
  ? varName
1040
1097
  : safeAccessor('input', key);
1041
1098
  fields.push(isRequired
1042
- ? ` ${safeKey(key)}: ${accessor},`
1043
- : ` ...(${accessor} !== undefined && { ${safeKey(key)}: ${accessor} }),`);
1099
+ ? ` ${safeLiteralKey(key)}: ${accessor},`
1100
+ : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
1044
1101
  }
1045
1102
  lines.push(` if (${deepGuard}) return {`);
1046
1103
  lines.push(fields.join('\n'));
@@ -1148,6 +1205,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
1148
1205
  * its imported parser when ref imports are on), and every other key is rejected
1149
1206
  * in strict mode or stripped in coerce mode — matching the interpreter.
1150
1207
  */
1208
+ /**
1209
+ * Emits a prototype-safe `result[key] = value` for the dynamic `for..in` copy
1210
+ * loops in the pattern-property parsers. A bare `result[key] = v` is a `[[Set]]`
1211
+ * — and for the key `"__proto__"` (own-enumerable when the input came from
1212
+ * `JSON.parse`) it invokes the inherited `Object.prototype` setter and reassigns
1213
+ * `result`'s prototype to attacker-controlled data, the same vector
1214
+ * `validateRecord` guards against. `"__proto__"` is the only own-enumerable key
1215
+ * whose name resolves to an accessor on `Object.prototype` (`constructor` et al.
1216
+ * are writable data properties, so a `[[Set]]` there just creates an own key),
1217
+ * so a single-keyed guard is sufficient. Each loop already runs a `.test(key)`
1218
+ * regex per iteration, which dominates the cost, so the extra `===` on the
1219
+ * common path is immeasurable. Assumes the target variable is named `result`,
1220
+ * as it is in every parser that calls this.
1221
+ */
1222
+ 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}; }`;
1151
1223
  const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict) => {
1152
1224
  const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
1153
1225
  // Below the inline threshold a key === "a" || key === "b" chain skips declared
@@ -1160,7 +1232,7 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1160
1232
  if (refPattern) {
1161
1233
  const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
1162
1234
  loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
1163
- loopLines.push(` (result as Record<string, unknown>)[key] = ${parserName}(input[key]);`);
1235
+ loopLines.push(` ${safeResultAssign('key', `${parserName}(input[key])`)}`);
1164
1236
  loopLines.push(` continue;`);
1165
1237
  loopLines.push(` }`);
1166
1238
  }
@@ -1169,7 +1241,7 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1169
1241
  .map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1170
1242
  if (keepConditions.length > 0) {
1171
1243
  loopLines.push(` if (${keepConditions.join(' || ')}) {`);
1172
- loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
1244
+ loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
1173
1245
  loopLines.push(` continue;`);
1174
1246
  loopLines.push(` }`);
1175
1247
  }
@@ -1183,10 +1255,22 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1183
1255
  const resultBody = propertyLines.length > 0 ? `{\n${propertyLines.join('\n')}\n }` : '{}';
1184
1256
  // Only the Set form needs a declaration; the inline === chain is stateless.
1185
1257
  const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};\n`).join('');
1258
+ // In strict mode assert the declared properties (type, required, enum,
1259
+ // constraints) — the `result` below is built from the *coercing* property
1260
+ // lines, which silently repair a wrong type and default a missing required key
1261
+ // instead of throwing. `generateObjectStrictAssertion` throws first; the
1262
+ // coercing build then runs only on already-valid input (a no-op there). It also
1263
+ // covers the object-level keyword checks, so it replaces the keyword-only
1264
+ // prelude. `.slice(1)` drops its `isObject` check (emitted just below). Coerce
1265
+ // mode (stripUnknown) keeps its lighter prelude.
1266
+ const assertionPrelude = strict
1267
+ ? generateObjectStrictAssertion(schema, typeName, { useRefImports, suffix }).slice(1)
1268
+ : [];
1269
+ const keywordPrelude = assertionPrelude.length > 0 ? `\n${assertionPrelude.join('\n')}` : '';
1186
1270
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1187
1271
  if (!isObject(input)) {
1188
1272
  ${notObjectBranch}
1189
- }
1273
+ }${keywordPrelude}
1190
1274
  ${knownKeysDeclaration} const result = ${resultBody} as unknown as ${typeName};
1191
1275
  for (const key in input) {
1192
1276
  ${loopLines.join('\n')}
@@ -1206,7 +1290,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1206
1290
  if (entry.isOptional) {
1207
1291
  return ` ${entry.value},`;
1208
1292
  }
1209
- return ` ${safeKey(entry.key)}: ${entry.value},`;
1293
+ return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
1210
1294
  });
1211
1295
  // Find the first pattern with a $ref for parser delegation
1212
1296
  if (!isSchemaObject(schema) || !('patternProperties' in schema)) {
@@ -1223,6 +1307,14 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1223
1307
  if (strictKeys) {
1224
1308
  return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict);
1225
1309
  }
1310
+ // `stripUnknown` (coerce mode) must keep keys a `patternProperties` regex
1311
+ // declares and drop only the genuinely-undeclared ones. Delegating to
1312
+ // `generateObjectParser` below would strip the pattern-matching keys too — its
1313
+ // strip logic only knows the declared `properties`. The selective combined
1314
+ // copy keeps declared + pattern-matching keys, exactly like the interpreter.
1315
+ if (stripUnknown && !strict) {
1316
+ return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, false);
1317
+ }
1226
1318
  if (!refPattern || !useRefImports) {
1227
1319
  return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1228
1320
  }
@@ -1241,10 +1333,11 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1241
1333
  const notObjectBranch = strict
1242
1334
  ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1243
1335
  : ` return {} as unknown as ${typeName};`;
1336
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1244
1337
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1245
1338
  if (!isObject(input)) {
1246
1339
  ${notObjectBranch}
1247
- }
1340
+ }${keywordPrelude}
1248
1341
  const result = {
1249
1342
  ${objectProperties}
1250
1343
  } as unknown as ${typeName};
@@ -1265,30 +1358,31 @@ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suf
1265
1358
  const additionalProps = schema.additionalProperties;
1266
1359
  // Check if additionalProps is defined before using it
1267
1360
  if (!additionalProps) {
1268
- return generateEmptyObjectParser(typeName, strict);
1361
+ return generateEmptyObjectParser(typeName, strict, schema);
1269
1362
  }
1363
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1270
1364
  // If additionalProperties is a $ref and useRefImports is true, generate a loop
1271
1365
  if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
1272
1366
  const ref = additionalProps.$ref;
1273
1367
  const parserName = generateParserName(refToName(ref, suffix));
1274
1368
  if (strict) {
1275
- 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};`;
1369
+ 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};`;
1276
1370
  }
1277
1371
  return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${parserName}) as ${typeName};`;
1278
1372
  }
1279
1373
  // Handle additionalProperties with a known type by generating inline validation
1280
1374
  if (isSchemaObject(additionalProps) && hasType(additionalProps)) {
1281
- const inlineParser = generateInlineValueParser(additionalProps);
1375
+ const inlineParser = generateInlineValueParser(additionalProps, strict, `[${typeName}]`);
1282
1376
  if (inlineParser) {
1283
1377
  if (strict) {
1284
- 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};`;
1378
+ 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};`;
1285
1379
  }
1286
1380
  return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${inlineParser}) as ${typeName};`;
1287
1381
  }
1288
1382
  }
1289
1383
  // Otherwise, just validate the input type and shallow copy
1290
1384
  if (strict) {
1291
- 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};`;
1385
+ 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};`;
1292
1386
  }
1293
1387
  return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {};`;
1294
1388
  };
@@ -1299,16 +1393,39 @@ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suf
1299
1393
  * Used to generate parsers for Record-like types where values have a known
1300
1394
  * primitive or array type (e.g. Record<string, string> or Record<string, string[]>).
1301
1395
  */
1302
- const generateInlineValueParser = (schema) => {
1396
+ const generateInlineValueParser = (schema, strict = false, label = '') => {
1303
1397
  if (!isSchemaObject(schema) || !hasType(schema)) {
1304
1398
  return null;
1305
1399
  }
1400
+ // Strict mode must *throw* on a wrong-typed record value, not repair it — the
1401
+ // coercing branch below would silently turn `{ a: 'x' }` into `{ a: 0 }` for a
1402
+ // `Record<string, number>`, which strict mode is documented never to do.
1403
+ if (strict) {
1404
+ const err = (t) => `throw new Error(${JSON.stringify(`${label} record value must be ${t}`)})`;
1405
+ switch (schema.type) {
1406
+ case 'string':
1407
+ return `(value: unknown) => { if (typeof value !== "string") ${err('string')}; return value; }`;
1408
+ case 'number':
1409
+ return `(value: unknown) => { if (typeof value !== "number") ${err('number')}; return value; }`;
1410
+ case 'integer':
1411
+ return `(value: unknown) => { if (typeof value !== "number" || !Number.isInteger(value)) ${err('integer')}; return value; }`;
1412
+ case 'boolean':
1413
+ return `(value: unknown) => { if (typeof value !== "boolean") ${err('boolean')}; return value; }`;
1414
+ case 'array':
1415
+ return `(value: unknown) => { if (!Array.isArray(value)) ${err('array')}; return value; }`;
1416
+ default:
1417
+ return null;
1418
+ }
1419
+ }
1306
1420
  switch (schema.type) {
1307
1421
  case 'string':
1308
1422
  return '(value: unknown) => typeof value === "string" ? value : ""';
1309
1423
  case 'number':
1310
- case 'integer':
1311
1424
  return '(value: unknown) => typeof value === "number" ? value : 0';
1425
+ case 'integer':
1426
+ // `integer` rejects non-integral numbers; a bare typeof accepts `1.5`,
1427
+ // matching every other integer site in this package (and strict mode).
1428
+ return '(value: unknown) => typeof value === "number" && Number.isInteger(value) ? value : 0';
1312
1429
  case 'boolean':
1313
1430
  return '(value: unknown) => typeof value === "boolean" ? value : false';
1314
1431
  case 'array':
@@ -1324,47 +1441,53 @@ const generateInlineValueParser = (schema) => {
1324
1441
  const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
1325
1442
  const functionName = generateParserName(typeName);
1326
1443
  if (!('patternProperties' in schema) || typeof schema.patternProperties !== 'object') {
1327
- return generateEmptyObjectParser(typeName, strict);
1444
+ return generateEmptyObjectParser(typeName, strict, schema);
1328
1445
  }
1329
1446
  const patternProps = schema.patternProperties;
1330
1447
  // Find the first pattern and its schema
1331
1448
  const patterns = Object.entries(patternProps);
1332
1449
  if (patterns.length === 0) {
1333
- return generateEmptyObjectParser(typeName, strict);
1450
+ return generateEmptyObjectParser(typeName, strict, schema);
1334
1451
  }
1335
1452
  const [pattern, patternSchema] = patterns[0];
1336
1453
  let patternAssignment = '(result as Record<string, unknown>)[key] = value;';
1454
+ // The value-expression form, used by the strictKeys path (which builds from
1455
+ // `{}`) to emit a prototype-safe assignment. `null` when no value is allowed.
1456
+ let patternValueExpr = 'value';
1337
1457
  // Use imported parser when pattern schema points to a $ref.
1338
1458
  if (useRefImports && isSchemaObject(patternSchema) && hasRef(patternSchema)) {
1339
1459
  const ref = patternSchema.$ref;
1340
1460
  const parserName = generateParserName(refToName(ref, suffix));
1341
1461
  patternAssignment = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
1462
+ patternValueExpr = `${parserName}(value)`;
1342
1463
  }
1343
1464
  else if (patternSchema === false) {
1344
1465
  // `false` means no values are allowed for matching keys.
1345
1466
  patternAssignment = '';
1467
+ patternValueExpr = null;
1346
1468
  }
1347
1469
  // Escape the pattern for safe inclusion in generated code
1348
1470
  const escapedPattern = escapeRegexPattern(pattern);
1349
1471
  const notObjectBranch = strict
1350
1472
  ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1351
1473
  : ` return {} as unknown as ${typeName};`;
1474
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1352
1475
  // With `additionalProperties: false`, only keys matching a pattern survive:
1353
1476
  // others are rejected (strict) or stripped (coerce). Start from an empty
1354
1477
  // object rather than spreading every input key.
1355
1478
  const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
1356
1479
  if (strictKeys) {
1357
1480
  const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
1358
- if (patternAssignment) {
1481
+ if (patternValueExpr) {
1359
1482
  loopLines.push(` const value = input[key];`);
1360
- loopLines.push(` ${patternAssignment}`);
1483
+ loopLines.push(` ${safeResultAssign('key', patternValueExpr)}`);
1361
1484
  }
1362
1485
  loopLines.push(` continue;`);
1363
1486
  loopLines.push(` }`);
1364
1487
  const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1365
1488
  if (keepConditions.length > 0) {
1366
1489
  loopLines.push(` if (${keepConditions.join(' || ')}) {`);
1367
- loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
1490
+ loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
1368
1491
  loopLines.push(` continue;`);
1369
1492
  loopLines.push(` }`);
1370
1493
  }
@@ -1373,7 +1496,7 @@ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix
1373
1496
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1374
1497
  if (!isObject(input)) {
1375
1498
  ${notObjectBranch}
1376
- }
1499
+ }${keywordPrelude}
1377
1500
  const result = {} as unknown as ${typeName};
1378
1501
  for (const key in input) {
1379
1502
  ${loopLines.join('\n')}
@@ -1385,7 +1508,7 @@ ${loopLines.join('\n')}
1385
1508
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1386
1509
  if (!isObject(input)) {
1387
1510
  ${notObjectBranch}
1388
- }
1511
+ }${keywordPrelude}
1389
1512
  const result = {
1390
1513
  ...input,
1391
1514
  } as unknown as ${typeName};
@@ -1597,17 +1720,17 @@ const selectParserStrategy = (schema, typeName, options) => {
1597
1720
  const additionalProps = schema.additionalProperties;
1598
1721
  // If additionalProperties is true or false, validate it is an object
1599
1722
  if (additionalProps === true || additionalProps === false) {
1600
- return generateEmptyObjectParser(typeName, strict);
1723
+ return generateEmptyObjectParser(typeName, strict, schema);
1601
1724
  }
1602
1725
  // Otherwise, handle as a schema (could be a $ref or object schema)
1603
1726
  return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1604
1727
  }
1605
1728
  // Handle empty object schemas
1606
1729
  if ('type' in schema && schema.type === 'object' && !hasProperties(schema)) {
1607
- return generateEmptyObjectParser(typeName, strict);
1730
+ return generateEmptyObjectParser(typeName, strict, schema);
1608
1731
  }
1609
1732
  // Default fallback - validate it is an object since we passed the isObjectSchema check
1610
- return generateEmptyObjectParser(typeName, strict);
1733
+ return generateEmptyObjectParser(typeName, strict, schema);
1611
1734
  };
1612
1735
  /**
1613
1736
  * Generates a safe parser as an arrow function that never throws.
@@ -1619,6 +1742,12 @@ const selectParserStrategy = (schema, typeName, options) => {
1619
1742
  * $ref will map each element through the imported parser.
1620
1743
  */
1621
1744
  export const generateParserFunction = (schema, typeName, options) => {
1745
+ // Strict parsers promise to throw on violations, so any keyword we cannot
1746
+ // enforce must fail loudly at generation rather than emit a permissive parser.
1747
+ // Coercing parsers are documented to repair rather than reject, so the guard
1748
+ // does not apply to them.
1749
+ if (options?.strict)
1750
+ assertNoUnsupportedKeywords(schema, typeName);
1622
1751
  return selectParserStrategy(schema, typeName, options);
1623
1752
  };
1624
1753
  /**
@@ -1645,6 +1774,12 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
1645
1774
  const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
1646
1775
  if (!isSchemaObject(schema))
1647
1776
  return stub;
1777
+ // Keywords the parser's fast path cannot mirror (contains, dependentRequired,
1778
+ // dependentSchemas, propertyNames) make a bare shape match insufficient: a
1779
+ // parent that fast-pathed through this predicate would skip their enforcement,
1780
+ // so stub out and force the parent's slow path (which calls the real parser).
1781
+ if (hasFastPathBlockingKeyword(schema))
1782
+ return stub;
1648
1783
  // An alias definition (a bare `$ref` with no shape of its own) delegates to
1649
1784
  // the referenced type's validator. Guarded against self-reference.
1650
1785
  if (useRefImports && hasRef(schema) && !hasProperties(schema)) {