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