@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.
- package/README.md +45 -0
- package/dist/generators/assert-supported-keywords.d.ts +26 -0
- package/dist/generators/assert-supported-keywords.js +74 -0
- package/dist/generators/generate-enum-check.js +16 -7
- package/dist/generators/generate-parser-function.js +189 -54
- package/dist/generators/generate-schema-checks.js +161 -1
- package/dist/generators/generate-strict-assertion.d.ts +22 -1
- package/dist/generators/generate-strict-assertion.js +247 -25
- package/dist/generators/generate-type-checks.d.ts +9 -0
- package/dist/generators/generate-type-checks.js +49 -1
- package/dist/generators/generate-validation-expression.d.ts +23 -0
- package/dist/generators/generate-validation-expression.js +81 -5
- package/dist/generators/subschema-match.d.ts +29 -0
- package/dist/generators/subschema-match.js +286 -0
- package/dist/helpers/get-default-value.js +8 -0
- package/package.json +2 -2
|
@@ -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 && { ${
|
|
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 && { ${
|
|
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 && { ${
|
|
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 ? {} : { ${
|
|
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(` ${
|
|
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
|
-
|
|
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}\`)
|
|
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
|
-
|
|
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(` ${
|
|
966
|
+
objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
|
|
910
967
|
}
|
|
911
968
|
else {
|
|
912
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
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(` ${
|
|
981
|
+
objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
|
|
925
982
|
}
|
|
926
983
|
else {
|
|
927
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
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(` ${
|
|
993
|
+
objectLines.push(` ${safeLiteralKey(key)}: ${parserName}(${accessor}),`);
|
|
937
994
|
}
|
|
938
995
|
else {
|
|
939
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
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(` ${
|
|
1006
|
+
objectLines.push(` ${safeLiteralKey(key)}: validateArray(${accessor}, ${parserName}),`);
|
|
950
1007
|
}
|
|
951
1008
|
else {
|
|
952
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
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(` ${
|
|
1019
|
+
objectLines.push(` ${safeLiteralKey(key)}: validateRecord(${accessor}, ${parserName}),`);
|
|
963
1020
|
}
|
|
964
1021
|
else {
|
|
965
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
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(` ${
|
|
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(` ${
|
|
1047
|
+
objectLines.push(` ${safeLiteralKey(key)}: ${valueExpr},`);
|
|
991
1048
|
}
|
|
992
1049
|
else {
|
|
993
|
-
objectLines.push(` ...(${accessor} !== undefined && { ${
|
|
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(` ${
|
|
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
|
-
? ` ${
|
|
1043
|
-
: ` ...(${accessor} !== undefined && { ${
|
|
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(` (
|
|
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(` (
|
|
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 ` ${
|
|
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}\`)
|
|
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}\`)
|
|
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}\`)
|
|
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 (
|
|
1481
|
+
if (patternValueExpr) {
|
|
1359
1482
|
loopLines.push(` const value = input[key];`);
|
|
1360
|
-
loopLines.push(` ${
|
|
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(` (
|
|
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)) {
|