@amritk/generate-parsers 0.16.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,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) {
@@ -456,6 +467,10 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
456
467
  !Array.isArray(schema.items)) {
457
468
  const items = schema.items;
458
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') : '';
459
474
  // validateArray identity-returns the input array when every element parses
460
475
  // to itself; this parser is EXPORTED, and exported parsers never alias the
461
476
  // value the caller passed in (matching the scalar root-array path's
@@ -463,7 +478,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
463
478
  // element references are still shared, like every other fast path.
464
479
  const delegatedBody = (itemParserName) => ` const _parsed = validateArray(input, ${itemParserName});\n return (_parsed === input ? [..._parsed] : _parsed) as ${typeName};`;
465
480
  const delegated = (itemParserName) => strict
466
- ? `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};`
467
482
  : `export const ${functionName} = (input: unknown): ${typeName} => {\n${delegatedBody(itemParserName)}\n};`;
468
483
  if (unionCtx?.useRefImports && isSchemaObject(items) && hasRef(items)) {
469
484
  return delegated(generateParserName(refToName(items.$ref, unionCtx.suffix)));
@@ -495,7 +510,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
495
510
  return `export const ${functionName} = (input: unknown): ${typeName} => null as ${typeName};`;
496
511
  }
497
512
  if (strict) {
498
- const assertion = generateScalarStrictAssertion(schema, typeName);
513
+ const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
499
514
  if (assertion === null) {
500
515
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
501
516
  }
@@ -513,6 +528,13 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
513
528
  case 'boolean':
514
529
  return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
515
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
+ }
516
538
  // Coerce each element when the item schema is a single scalar type or an enum.
517
539
  if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
518
540
  const item = schema.items;
@@ -525,15 +547,33 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
525
547
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
526
548
  }
527
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
+ };
528
566
  /**
529
567
  * Generates a parser for empty object schemas or schemas with only additionalProperties.
530
568
  * 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.
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.
532
571
  */
533
- const generateEmptyObjectParser = (typeName, strict) => {
572
+ const generateEmptyObjectParser = (typeName, strict, schema) => {
534
573
  const functionName = generateParserName(typeName);
535
574
  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};`;
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};`;
537
577
  }
538
578
  return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {} as ${typeName};`;
539
579
  };
@@ -674,7 +714,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
674
714
  const exportPrefix = exported ? 'export ' : '';
675
715
  if (!hasProperties(schema)) {
676
716
  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};`;
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};`;
678
718
  }
679
719
  return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
680
720
  }
@@ -750,7 +790,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
750
790
  isSchemaObject(schema) &&
751
791
  hasAllOf(schema) &&
752
792
  schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
753
- 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);
754
799
  const fastPathChecks = [];
755
800
  // `toVarName` collapses distinct keys that differ only in non-identifier chars
756
801
  // (e.g. `a-b` and `a.b` both → `_a_b`), which would emit two `const _a_b`
@@ -819,7 +864,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
819
864
  // stripUnknown input — which carries extras the build removes, and nested
820
865
  // extras each sub-parser removes — while the cold path's per-property
821
866
  // assertions still produce the precise error on a genuine mismatch.
822
- let canShallowGuard = strict && stripUnknown && !strictKeys;
867
+ let canShallowGuard = strict && stripUnknown && !strictKeys && !hasFastPathBlockingKeyword(schema);
823
868
  const shallowChecks = [];
824
869
  if (canShallowGuard) {
825
870
  for (const { key, varName, isRequired, propSchema } of propInfo) {
@@ -906,10 +951,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
906
951
  if (subName) {
907
952
  const subParserName = generateParserName(subName);
908
953
  if (isRequired) {
909
- objectLines.push(` ${safeKey(key)}: ${subParserName}(${accessor}),`);
954
+ objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
910
955
  }
911
956
  else {
912
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${subParserName}(${accessor}) }),`);
957
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
913
958
  }
914
959
  continue;
915
960
  }
@@ -921,10 +966,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
921
966
  if (itemSubName) {
922
967
  const itemParserName = generateParserName(itemSubName);
923
968
  if (isRequired) {
924
- objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
969
+ objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
925
970
  }
926
971
  else {
927
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
972
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
928
973
  }
929
974
  continue;
930
975
  }
@@ -933,10 +978,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
933
978
  const ref = propSchema.$ref;
934
979
  const parserName = generateParserName(refToName(ref, suffix));
935
980
  if (isRequired) {
936
- objectLines.push(` ${safeKey(key)}: ${parserName}(${accessor}),`);
981
+ objectLines.push(` ${safeLiteralKey(key)}: ${parserName}(${accessor}),`);
937
982
  }
938
983
  else {
939
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${parserName}(${accessor}) }),`);
984
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${parserName}(${accessor}) }),`);
940
985
  }
941
986
  continue;
942
987
  }
@@ -946,10 +991,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
946
991
  const ref = items.$ref;
947
992
  const parserName = generateParserName(refToName(ref, suffix));
948
993
  if (isRequired) {
949
- objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${parserName}),`);
994
+ objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}),`);
950
995
  }
951
996
  else {
952
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
997
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}) }),`);
953
998
  }
954
999
  continue;
955
1000
  }
@@ -959,17 +1004,17 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
959
1004
  const ref = additionalProps.$ref;
960
1005
  const parserName = generateParserName(refToName(ref, suffix));
961
1006
  if (isRequired) {
962
- objectLines.push(` ${safeKey(key)}: validateRecord(${accessor}, ${parserName}),`);
1007
+ objectLines.push(` ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}),`);
963
1008
  }
964
1009
  else {
965
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
1010
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}) }),`);
966
1011
  }
967
1012
  continue;
968
1013
  }
969
1014
  // Handle non-schema-object properties
970
1015
  if (!isSchemaObject(propSchema)) {
971
1016
  if (isRequired) {
972
- objectLines.push(` ${safeKey(key)}: undefined,`);
1017
+ objectLines.push(` ${safeLiteralKey(key)}: undefined,`);
973
1018
  }
974
1019
  continue;
975
1020
  }
@@ -987,10 +1032,10 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
987
1032
  valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined, caseInsensitive);
988
1033
  }
989
1034
  if (isRequired) {
990
- objectLines.push(` ${safeKey(key)}: ${valueExpr},`);
1035
+ objectLines.push(` ${safeLiteralKey(key)}: ${valueExpr},`);
991
1036
  }
992
1037
  else {
993
- objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${valueExpr} }),`);
1038
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${valueExpr} }),`);
994
1039
  }
995
1040
  }
996
1041
  return objectLines;
@@ -1032,15 +1077,15 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
1032
1077
  for (const { key, varName, isRequired, propSchema } of propInfo) {
1033
1078
  if (!isSchemaObject(propSchema)) {
1034
1079
  if (isRequired)
1035
- fields.push(` ${safeKey(key)}: undefined,`);
1080
+ fields.push(` ${safeLiteralKey(key)}: undefined,`);
1036
1081
  continue;
1037
1082
  }
1038
1083
  const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports)
1039
1084
  ? varName
1040
1085
  : safeAccessor('input', key);
1041
1086
  fields.push(isRequired
1042
- ? ` ${safeKey(key)}: ${accessor},`
1043
- : ` ...(${accessor} !== undefined && { ${safeKey(key)}: ${accessor} }),`);
1087
+ ? ` ${safeLiteralKey(key)}: ${accessor},`
1088
+ : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
1044
1089
  }
1045
1090
  lines.push(` if (${deepGuard}) return {`);
1046
1091
  lines.push(fields.join('\n'));
@@ -1148,6 +1193,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
1148
1193
  * its imported parser when ref imports are on), and every other key is rejected
1149
1194
  * in strict mode or stripped in coerce mode — matching the interpreter.
1150
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}; }`;
1151
1211
  const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict) => {
1152
1212
  const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
1153
1213
  // Below the inline threshold a key === "a" || key === "b" chain skips declared
@@ -1160,7 +1220,7 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1160
1220
  if (refPattern) {
1161
1221
  const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
1162
1222
  loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
1163
- loopLines.push(` (result as Record<string, unknown>)[key] = ${parserName}(input[key]);`);
1223
+ loopLines.push(` ${safeResultAssign('key', `${parserName}(input[key])`)}`);
1164
1224
  loopLines.push(` continue;`);
1165
1225
  loopLines.push(` }`);
1166
1226
  }
@@ -1169,7 +1229,7 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1169
1229
  .map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1170
1230
  if (keepConditions.length > 0) {
1171
1231
  loopLines.push(` if (${keepConditions.join(' || ')}) {`);
1172
- loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
1232
+ loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
1173
1233
  loopLines.push(` continue;`);
1174
1234
  loopLines.push(` }`);
1175
1235
  }
@@ -1183,10 +1243,11 @@ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLi
1183
1243
  const resultBody = propertyLines.length > 0 ? `{\n${propertyLines.join('\n')}\n }` : '{}';
1184
1244
  // Only the Set form needs a declaration; the inline === chain is stateless.
1185
1245
  const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};\n`).join('');
1246
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1186
1247
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1187
1248
  if (!isObject(input)) {
1188
1249
  ${notObjectBranch}
1189
- }
1250
+ }${keywordPrelude}
1190
1251
  ${knownKeysDeclaration} const result = ${resultBody} as unknown as ${typeName};
1191
1252
  for (const key in input) {
1192
1253
  ${loopLines.join('\n')}
@@ -1206,7 +1267,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1206
1267
  if (entry.isOptional) {
1207
1268
  return ` ${entry.value},`;
1208
1269
  }
1209
- return ` ${safeKey(entry.key)}: ${entry.value},`;
1270
+ return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
1210
1271
  });
1211
1272
  // Find the first pattern with a $ref for parser delegation
1212
1273
  if (!isSchemaObject(schema) || !('patternProperties' in schema)) {
@@ -1241,10 +1302,11 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1241
1302
  const notObjectBranch = strict
1242
1303
  ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1243
1304
  : ` return {} as unknown as ${typeName};`;
1305
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1244
1306
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1245
1307
  if (!isObject(input)) {
1246
1308
  ${notObjectBranch}
1247
- }
1309
+ }${keywordPrelude}
1248
1310
  const result = {
1249
1311
  ${objectProperties}
1250
1312
  } as unknown as ${typeName};
@@ -1265,14 +1327,15 @@ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suf
1265
1327
  const additionalProps = schema.additionalProperties;
1266
1328
  // Check if additionalProps is defined before using it
1267
1329
  if (!additionalProps) {
1268
- return generateEmptyObjectParser(typeName, strict);
1330
+ return generateEmptyObjectParser(typeName, strict, schema);
1269
1331
  }
1332
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1270
1333
  // If additionalProperties is a $ref and useRefImports is true, generate a loop
1271
1334
  if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
1272
1335
  const ref = additionalProps.$ref;
1273
1336
  const parserName = generateParserName(refToName(ref, suffix));
1274
1337
  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};`;
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};`;
1276
1339
  }
1277
1340
  return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${parserName}) as ${typeName};`;
1278
1341
  }
@@ -1281,14 +1344,14 @@ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suf
1281
1344
  const inlineParser = generateInlineValueParser(additionalProps);
1282
1345
  if (inlineParser) {
1283
1346
  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};`;
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};`;
1285
1348
  }
1286
1349
  return `export const ${functionName} = (input: unknown): ${typeName} => validateRecord(input, ${inlineParser}) as ${typeName};`;
1287
1350
  }
1288
1351
  }
1289
1352
  // Otherwise, just validate the input type and shallow copy
1290
1353
  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};`;
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};`;
1292
1355
  }
1293
1356
  return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? { ...input } as ${typeName} : {};`;
1294
1357
  };
@@ -1307,8 +1370,11 @@ const generateInlineValueParser = (schema) => {
1307
1370
  case 'string':
1308
1371
  return '(value: unknown) => typeof value === "string" ? value : ""';
1309
1372
  case 'number':
1310
- case 'integer':
1311
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';
1312
1378
  case 'boolean':
1313
1379
  return '(value: unknown) => typeof value === "boolean" ? value : false';
1314
1380
  case 'array':
@@ -1324,47 +1390,53 @@ const generateInlineValueParser = (schema) => {
1324
1390
  const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
1325
1391
  const functionName = generateParserName(typeName);
1326
1392
  if (!('patternProperties' in schema) || typeof schema.patternProperties !== 'object') {
1327
- return generateEmptyObjectParser(typeName, strict);
1393
+ return generateEmptyObjectParser(typeName, strict, schema);
1328
1394
  }
1329
1395
  const patternProps = schema.patternProperties;
1330
1396
  // Find the first pattern and its schema
1331
1397
  const patterns = Object.entries(patternProps);
1332
1398
  if (patterns.length === 0) {
1333
- return generateEmptyObjectParser(typeName, strict);
1399
+ return generateEmptyObjectParser(typeName, strict, schema);
1334
1400
  }
1335
1401
  const [pattern, patternSchema] = patterns[0];
1336
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';
1337
1406
  // Use imported parser when pattern schema points to a $ref.
1338
1407
  if (useRefImports && isSchemaObject(patternSchema) && hasRef(patternSchema)) {
1339
1408
  const ref = patternSchema.$ref;
1340
1409
  const parserName = generateParserName(refToName(ref, suffix));
1341
1410
  patternAssignment = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
1411
+ patternValueExpr = `${parserName}(value)`;
1342
1412
  }
1343
1413
  else if (patternSchema === false) {
1344
1414
  // `false` means no values are allowed for matching keys.
1345
1415
  patternAssignment = '';
1416
+ patternValueExpr = null;
1346
1417
  }
1347
1418
  // Escape the pattern for safe inclusion in generated code
1348
1419
  const escapedPattern = escapeRegexPattern(pattern);
1349
1420
  const notObjectBranch = strict
1350
1421
  ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
1351
1422
  : ` return {} as unknown as ${typeName};`;
1423
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : '';
1352
1424
  // With `additionalProperties: false`, only keys matching a pattern survive:
1353
1425
  // others are rejected (strict) or stripped (coerce). Start from an empty
1354
1426
  // object rather than spreading every input key.
1355
1427
  const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
1356
1428
  if (strictKeys) {
1357
1429
  const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
1358
- if (patternAssignment) {
1430
+ if (patternValueExpr) {
1359
1431
  loopLines.push(` const value = input[key];`);
1360
- loopLines.push(` ${patternAssignment}`);
1432
+ loopLines.push(` ${safeResultAssign('key', patternValueExpr)}`);
1361
1433
  }
1362
1434
  loopLines.push(` continue;`);
1363
1435
  loopLines.push(` }`);
1364
1436
  const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1365
1437
  if (keepConditions.length > 0) {
1366
1438
  loopLines.push(` if (${keepConditions.join(' || ')}) {`);
1367
- loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
1439
+ loopLines.push(` ${safeResultAssign('key', 'input[key]')}`);
1368
1440
  loopLines.push(` continue;`);
1369
1441
  loopLines.push(` }`);
1370
1442
  }
@@ -1373,7 +1445,7 @@ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix
1373
1445
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1374
1446
  if (!isObject(input)) {
1375
1447
  ${notObjectBranch}
1376
- }
1448
+ }${keywordPrelude}
1377
1449
  const result = {} as unknown as ${typeName};
1378
1450
  for (const key in input) {
1379
1451
  ${loopLines.join('\n')}
@@ -1385,7 +1457,7 @@ ${loopLines.join('\n')}
1385
1457
  return `export const ${functionName} = (input: unknown): ${typeName} => {
1386
1458
  if (!isObject(input)) {
1387
1459
  ${notObjectBranch}
1388
- }
1460
+ }${keywordPrelude}
1389
1461
  const result = {
1390
1462
  ...input,
1391
1463
  } as unknown as ${typeName};
@@ -1597,17 +1669,17 @@ const selectParserStrategy = (schema, typeName, options) => {
1597
1669
  const additionalProps = schema.additionalProperties;
1598
1670
  // If additionalProperties is true or false, validate it is an object
1599
1671
  if (additionalProps === true || additionalProps === false) {
1600
- return generateEmptyObjectParser(typeName, strict);
1672
+ return generateEmptyObjectParser(typeName, strict, schema);
1601
1673
  }
1602
1674
  // Otherwise, handle as a schema (could be a $ref or object schema)
1603
1675
  return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1604
1676
  }
1605
1677
  // Handle empty object schemas
1606
1678
  if ('type' in schema && schema.type === 'object' && !hasProperties(schema)) {
1607
- return generateEmptyObjectParser(typeName, strict);
1679
+ return generateEmptyObjectParser(typeName, strict, schema);
1608
1680
  }
1609
1681
  // Default fallback - validate it is an object since we passed the isObjectSchema check
1610
- return generateEmptyObjectParser(typeName, strict);
1682
+ return generateEmptyObjectParser(typeName, strict, schema);
1611
1683
  };
1612
1684
  /**
1613
1685
  * Generates a safe parser as an arrow function that never throws.
@@ -1619,6 +1691,12 @@ const selectParserStrategy = (schema, typeName, options) => {
1619
1691
  * $ref will map each element through the imported parser.
1620
1692
  */
1621
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);
1622
1700
  return selectParserStrategy(schema, typeName, options);
1623
1701
  };
1624
1702
  /**
@@ -1645,6 +1723,12 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
1645
1723
  const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
1646
1724
  if (!isSchemaObject(schema))
1647
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;
1648
1732
  // An alias definition (a bare `$ref` with no shape of its own) delegates to
1649
1733
  // the referenced type's validator. Guarded against self-reference.
1650
1734
  if (useRefImports && hasRef(schema) && !hasProperties(schema)) {