@amritk/generate-parsers 0.15.0 → 0.16.0

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 CHANGED
@@ -134,6 +134,14 @@ Returns: `Promise<GeneratedFile[]>`.
134
134
  <tr>
135
135
  <td colspan="3">Build each parser's result from the schema's declared properties only, silently dropping undeclared input keys at every nesting level (zod's .strip()). Extras are never a validation error, so this composes with strict (which still throws on wrong types and missing required properties) and yields to additionalProperties: false, which rejects rather than strips in strict mode.</td>
136
136
  </tr>
137
+ <tr>
138
+ <td>🔡 <code>caseInsensitive</code></td>
139
+ <td><code>boolean</code></td>
140
+ <td align="center"><code>false</code></td>
141
+ </tr>
142
+ <tr>
143
+ <td colspan="3">Normalize a mis-cased string to the exact casing of a declared enum/const member it matches case-insensitively (e.g. hElLo → hello) instead of coercing to the default. Coerce mode only — strict parsers still reject a casing mismatch. Correctly-cased input keeps the exact-match fast path, so the hot path is unaffected.</td>
144
+ </tr>
137
145
  </tbody>
138
146
  </table>
139
147
  <!-- config-table-end -->
@@ -53,6 +53,12 @@ export type GeneratedFile = {
53
53
  * stripping. Note the `mjst` CLI defaults this to `'ts'` (falling back to `'js'` only
54
54
  * under `--build`, where tsc must compile the sources) so generated output runs under
55
55
  * Node without a build step; direct callers of `buildSchema` opt in explicitly.
56
+ * @param caseInsensitive - When true, the generated coercing parsers normalize a mis-cased
57
+ * string to the exact casing of a declared `enum`/`const` member it matches
58
+ * case-insensitively (e.g. `hElLo` → `hello`) instead of coercing to the default. Coerce
59
+ * mode only — strict parsers still reject a casing mismatch. The normalization lives on
60
+ * the coercion failure branch, so correctly-cased input keeps the exact-match fast path
61
+ * and the hot path is unaffected.
56
62
  * @returns An array of generated TypeScript files
57
63
  *
58
64
  * @example
@@ -85,4 +91,4 @@ export type GeneratedFile = {
85
91
  * });
86
92
  * ```
87
93
  */
88
- export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean, stripUnknown?: boolean, typeSuffix?: string, importExt?: ImportExtension) => Promise<GeneratedFile[]>;
94
+ export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean, stripUnknown?: boolean, typeSuffix?: string, importExt?: ImportExtension, caseInsensitive?: boolean) => Promise<GeneratedFile[]>;
@@ -89,6 +89,12 @@ const readHelperSource = async (helper, importExt) => {
89
89
  * stripping. Note the `mjst` CLI defaults this to `'ts'` (falling back to `'js'` only
90
90
  * under `--build`, where tsc must compile the sources) so generated output runs under
91
91
  * Node without a build step; direct callers of `buildSchema` opt in explicitly.
92
+ * @param caseInsensitive - When true, the generated coercing parsers normalize a mis-cased
93
+ * string to the exact casing of a declared `enum`/`const` member it matches
94
+ * case-insensitively (e.g. `hElLo` → `hello`) instead of coercing to the default. Coerce
95
+ * mode only — strict parsers still reject a casing mismatch. The normalization lives on
96
+ * the coercion failure branch, so correctly-cased input keeps the exact-match fast path
97
+ * and the hot path is unaffected.
92
98
  * @returns An array of generated TypeScript files
93
99
  *
94
100
  * @example
@@ -121,7 +127,7 @@ const readHelperSource = async (helper, importExt) => {
121
127
  * });
122
128
  * ```
123
129
  */
124
- export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = 'package', helpersImportPrefix = './', readonly = false, stripUnknown = false, typeSuffix = '', importExt = 'js') => {
130
+ export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = 'package', helpersImportPrefix = './', readonly = false, stripUnknown = false, typeSuffix = '', importExt = 'js', caseInsensitive = false) => {
125
131
  const files = [];
126
132
  const usedHelpers = new Set();
127
133
  walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
@@ -145,6 +151,7 @@ export const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnl
145
151
  ...(logWarnings !== undefined ? { logWarnings } : {}),
146
152
  ...(strict !== undefined ? { strict } : {}),
147
153
  ...(stripUnknown ? { stripUnknown } : {}),
154
+ ...(caseInsensitive ? { caseInsensitive } : {}),
148
155
  });
149
156
  files.push({ filename: `${node.filename}.ts`, content: result.content });
150
157
  for (const helper of result.usedHelpers)
@@ -11,3 +11,20 @@
11
11
  * differs from `===`), so the verdict is unchanged.
12
12
  */
13
13
  export declare const generateEnumCheck: (accessor: string, enumValues: readonly unknown[]) => string;
14
+ /**
15
+ * Builds a case-insensitive coercion expression for a string `enum`/`const`
16
+ * value whose exact-match check has already failed. Only the *string* members
17
+ * participate (a number/boolean/null member has no case to fold): a value that
18
+ * matches one case-insensitively normalizes to that member's exact casing, and
19
+ * everything else yields `fallback`.
20
+ *
21
+ * This is emitted ONLY on the already-invalid branch of the coercion ternary, so
22
+ * a correctly-cased value never reaches it — the exact `===` fast path (and the
23
+ * shape validators / deep guards built on it) is unchanged, so the hot path pays
24
+ * nothing. Only a mis-cased value, which today coerces straight to the default,
25
+ * now takes a single `toLowerCase()` and an O(1) map lookup instead.
26
+ *
27
+ * Returns `null` when no member is a string (nothing to fold), so the caller
28
+ * keeps its plain fallback and emits no extra code.
29
+ */
30
+ export declare const generateEnumCaseInsensitiveCoercion: (accessor: string, enumValues: readonly unknown[], fallback: string) => string | null;
@@ -20,3 +20,35 @@ export const generateEnumCheck = (accessor, enumValues) => {
20
20
  const serializedEnum = JSON.stringify(enumValues);
21
21
  return `${serializedEnum}.includes(${accessor} as never)`;
22
22
  };
23
+ /**
24
+ * Builds a case-insensitive coercion expression for a string `enum`/`const`
25
+ * value whose exact-match check has already failed. Only the *string* members
26
+ * participate (a number/boolean/null member has no case to fold): a value that
27
+ * matches one case-insensitively normalizes to that member's exact casing, and
28
+ * everything else yields `fallback`.
29
+ *
30
+ * This is emitted ONLY on the already-invalid branch of the coercion ternary, so
31
+ * a correctly-cased value never reaches it — the exact `===` fast path (and the
32
+ * shape validators / deep guards built on it) is unchanged, so the hot path pays
33
+ * nothing. Only a mis-cased value, which today coerces straight to the default,
34
+ * now takes a single `toLowerCase()` and an O(1) map lookup instead.
35
+ *
36
+ * Returns `null` when no member is a string (nothing to fold), so the caller
37
+ * keeps its plain fallback and emits no extra code.
38
+ */
39
+ export const generateEnumCaseInsensitiveCoercion = (accessor, enumValues, fallback) => {
40
+ const byLower = {};
41
+ for (const value of enumValues) {
42
+ if (typeof value !== 'string')
43
+ continue;
44
+ const lower = value.toLowerCase();
45
+ // First writer wins, so declaration order breaks ties between members that
46
+ // fold to the same key (e.g. `"on"` and `"ON"`).
47
+ if (!(lower in byLower))
48
+ byLower[lower] = value;
49
+ }
50
+ if (Object.keys(byLower).length === 0)
51
+ return null;
52
+ const map = JSON.stringify(byLower);
53
+ return `(typeof ${accessor} === "string" ? ((${map} as Record<string, string>)[(${accessor} as string).toLowerCase()] ?? ${fallback}) : ${fallback})`;
54
+ };
@@ -78,6 +78,13 @@ type GenerateFileOptions = {
78
78
  * user-named root.
79
79
  */
80
80
  readonly isRoot?: boolean;
81
+ /**
82
+ * When true, a mis-cased string that matches a declared `enum`/`const` member
83
+ * case-insensitively is normalized to that member's exact casing (coerce mode
84
+ * only). The normalization is emitted on the coercion failure branch, so
85
+ * already-valid input keeps the exact-match fast path.
86
+ */
87
+ readonly caseInsensitive?: boolean;
81
88
  };
82
89
  /** Result of generating a single parser file. */
83
90
  export type GeneratedFileContent = {
@@ -67,6 +67,7 @@ export const generateFile = (schema, typeName, options) => {
67
67
  ...(options?.logWarnings !== undefined ? { logWarnings: options.logWarnings } : {}),
68
68
  ...(options?.strict !== undefined ? { strict: options.strict } : {}),
69
69
  ...(options?.stripUnknown !== undefined ? { stripUnknown: options.stripUnknown } : {}),
70
+ ...(options?.caseInsensitive !== undefined ? { caseInsensitive: options.caseInsensitive } : {}),
70
71
  });
71
72
  const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown, reservedNames);
72
73
  const combinedFunctions = `${shapeValidator}\n\n${parserFunction}`;
@@ -58,6 +58,15 @@ type GenerateParserOptions = {
58
58
  * generate a validation-free pass-through parser.
59
59
  */
60
60
  readonly isRoot?: boolean;
61
+ /**
62
+ * When true, a mis-cased string that matches a declared `enum`/`const` member
63
+ * case-insensitively is normalized to that member's exact casing (e.g. `hElLo`
64
+ * → `hello`) instead of coercing to the default. Coerce mode only — strict
65
+ * parsers still reject a casing mismatch. The normalization lives on the
66
+ * failure branch of the coercion ternary, so a correctly-cased value keeps the
67
+ * exact `===` fast path and the hot path is unaffected.
68
+ */
69
+ readonly caseInsensitive?: boolean;
61
70
  };
62
71
  /**
63
72
  * Generates a safe parser as an arrow function that never throws.
@@ -8,6 +8,7 @@ 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 { generateEnumCaseInsensitiveCoercion } from './generate-enum-check.js';
11
12
  import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
12
13
  import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName, } from './generate-type-checks.js';
13
14
  import { generateValidationExpression, isCoercibleItemSchema } from './generate-validation-expression.js';
@@ -147,7 +148,7 @@ const shouldUseRecordRefImport = (propSchema, useRefImports) => {
147
148
  /**
148
149
  * Generates the value expression for a property based on its schema and options.
149
150
  */
150
- const generatePropertyValue = (key, propSchema, isRequired, useRefImports, suffix) => {
151
+ const generatePropertyValue = (key, propSchema, isRequired, useRefImports, suffix, caseInsensitive = false) => {
151
152
  // Handle direct $ref properties
152
153
  if (shouldUseRefImport(propSchema, useRefImports)) {
153
154
  const ref = propSchema.$ref;
@@ -173,20 +174,20 @@ const generatePropertyValue = (key, propSchema, isRequired, useRefImports, suffi
173
174
  }
174
175
  // Generate standard validation expression
175
176
  const defaultValue = getDefaultValue(propSchema);
176
- const valueExpression = generateValidationExpression(key, propSchema, defaultValue, isRequired);
177
+ const valueExpression = generateValidationExpression(key, propSchema, defaultValue, isRequired, undefined, undefined, undefined, undefined, caseInsensitive);
177
178
  return isRequired ? valueExpression : generateOptionalInlineProperty(key, valueExpression);
178
179
  };
179
180
  /**
180
181
  * Generates property entries for all properties in the schema.
181
182
  */
182
- const generatePropertyEntries = (schema, useRefImports, suffix) => {
183
+ const generatePropertyEntries = (schema, useRefImports, suffix, caseInsensitive = false) => {
183
184
  if (!hasProperties(schema)) {
184
185
  return [];
185
186
  }
186
187
  const entries = [];
187
188
  for (const [key, propSchema] of Object.entries(schema.properties)) {
188
189
  const isRequired = isPropertyRequired(key, schema);
189
- const value = generatePropertyValue(key, propSchema, isRequired, useRefImports, suffix);
190
+ const value = generatePropertyValue(key, propSchema, isRequired, useRefImports, suffix, caseInsensitive);
190
191
  // Determine if the property uses spread syntax (making it optional in the object literal)
191
192
  const isOptional = value.startsWith('...');
192
193
  entries.push({ key, value, isOptional });
@@ -419,7 +420,11 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
419
420
  if (hasEnum(schema) && schema.enum.length > 0) {
420
421
  const values = JSON.stringify(schema.enum);
421
422
  const fallback = JSON.stringify(schema.enum[0]);
422
- return `export const ${functionName} = (input: unknown): ${typeName} => ${values}.includes(input as never) ? input as ${typeName} : ${fallback} as ${typeName};`;
423
+ // Case-insensitive normalization sits on the non-member branch only, so an
424
+ // exact member still returns via the `includes` fast path untouched.
425
+ const ci = unionCtx?.caseInsensitive ? generateEnumCaseInsensitiveCoercion('input', schema.enum, fallback) : null;
426
+ const coerced = ci ? `(${ci})` : fallback;
427
+ return `export const ${functionName} = (input: unknown): ${typeName} => ${values}.includes(input as never) ? input as ${typeName} : ${coerced} as ${typeName};`;
423
428
  }
424
429
  // A top-level union must validate membership: an unmatched value is not of
425
430
  // the declared union type, so coerce it to a member-shaped default. Reuse the
@@ -477,7 +482,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
477
482
  const preamble = [
478
483
  `type ${itemName} = ${typeName}[number];`,
479
484
  generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved),
480
- generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved),
485
+ generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false),
481
486
  ].join('\n\n');
482
487
  return `${preamble}\n\n${delegated(generateParserName(itemName))}`;
483
488
  }
@@ -511,7 +516,7 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
511
516
  // Coerce each element when the item schema is a single scalar type or an enum.
512
517
  if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
513
518
  const item = schema.items;
514
- const itemExpr = generateValidationExpression('', item, getDefaultValue(item), true, undefined, undefined, '_it', true);
519
+ const itemExpr = generateValidationExpression('', item, getDefaultValue(item), true, undefined, undefined, '_it', true, unionCtx?.caseInsensitive);
515
520
  return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? (input as unknown[]).map((_it) => ${itemExpr}) as ${typeName} : [] as ${typeName};`;
516
521
  }
517
522
  return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? [...input] as ${typeName} : [] as ${typeName};`;
@@ -664,7 +669,7 @@ const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
664
669
  * - Fast path that returns input directly when all properties are already valid
665
670
  * - Slow path with optimized expressions
666
671
  */
667
- const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES) => {
672
+ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false) => {
668
673
  const functionName = generateParserName(typeName);
669
674
  const exportPrefix = exported ? 'export ' : '';
670
675
  if (!hasProperties(schema)) {
@@ -690,7 +695,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
690
695
  const propSchema = schemaProps[key];
691
696
  preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
692
697
  preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames));
693
- preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames));
698
+ preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive));
694
699
  }
695
700
  for (const [key, subName] of subItemNames) {
696
701
  const propSchema = schemaProps[key];
@@ -701,7 +706,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
701
706
  // Hand-rolled loop instead of Array.prototype.every on the guard path — the
702
707
  // callback protocol costs a few percent on element-heavy hot paths.
703
708
  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));
709
+ preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive));
705
710
  }
706
711
  // additionalProperties: false — the per-key "is this undeclared" test inlines
707
712
  // `!==` comparisons for a short key list and hoists a Set only for a long one
@@ -979,7 +984,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
979
984
  // For optional properties we know the value is not undefined because
980
985
  // we're inside the `...(accessor !== undefined && { ... })` check.
981
986
  const knownNotUndefined = !isRequired;
982
- valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined);
987
+ valueExpr = generateValidationExpression(key, propSchema, defaultValue, true, undefined, undefined, shouldCache ? varName : undefined, knownNotUndefined, caseInsensitive);
983
988
  }
984
989
  if (isRequired) {
985
990
  objectLines.push(` ${safeKey(key)}: ${valueExpr},`);
@@ -1193,9 +1198,9 @@ ${loopLines.join('\n')}
1193
1198
  * Generates a parser for schemas that have both properties AND patternProperties.
1194
1199
  * Parses the known properties first, then iterates remaining keys to match patterns.
1195
1200
  */
1196
- const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false) => {
1201
+ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false, caseInsensitive = false) => {
1197
1202
  const functionName = generateParserName(typeName);
1198
- const entries = generatePropertyEntries(schema, useRefImports, suffix);
1203
+ const entries = generatePropertyEntries(schema, useRefImports, suffix, caseInsensitive);
1199
1204
  // Build the known property lines for the initial object
1200
1205
  const propertyLines = entries.map((entry) => {
1201
1206
  if (entry.isOptional) {
@@ -1205,7 +1210,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1205
1210
  });
1206
1211
  // Find the first pattern with a $ref for parser delegation
1207
1212
  if (!isSchemaObject(schema) || !('patternProperties' in schema)) {
1208
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
1213
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1209
1214
  }
1210
1215
  const patternProps = schema.patternProperties;
1211
1216
  const patterns = Object.entries(patternProps);
@@ -1219,7 +1224,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
1219
1224
  return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict);
1220
1225
  }
1221
1226
  if (!refPattern || !useRefImports) {
1222
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
1227
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1223
1228
  }
1224
1229
  const [pattern, patternSchema] = refPattern;
1225
1230
  const ref = patternSchema.$ref;
@@ -1398,7 +1403,7 @@ ${notObjectBranch}
1398
1403
  * This always generates parser calls regardless of useRefImports setting,
1399
1404
  * because conditional logic requires delegating to the appropriate parser.
1400
1405
  */
1401
- const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false) => {
1406
+ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = false, caseInsensitive = false) => {
1402
1407
  const functionName = generateParserName(typeName);
1403
1408
  // Extract the condition and branches
1404
1409
  const ifSchema = schema.if;
@@ -1413,7 +1418,7 @@ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = fals
1413
1418
  // regular object parser from the merged property set.
1414
1419
  const mergedSchema = getConditionalObjectSchema(schema);
1415
1420
  if (mergedSchema) {
1416
- return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown);
1421
+ return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1417
1422
  }
1418
1423
  return generateEmptyObjectParser(typeName);
1419
1424
  }
@@ -1427,7 +1432,7 @@ const generateConditionalParser = (schema, typeName, suffix, stripUnknown = fals
1427
1432
  // Condition is not a $ref check — flatten into an object parser
1428
1433
  const mergedSchema = getConditionalObjectSchema(schema);
1429
1434
  if (mergedSchema) {
1430
- return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown);
1435
+ return generateObjectParser(mergedSchema, typeName, false, suffix, false, false, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1431
1436
  }
1432
1437
  return generateEmptyObjectParser(typeName);
1433
1438
  }
@@ -1523,6 +1528,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1523
1528
  const logWarnings = options?.logWarnings ?? false;
1524
1529
  const strict = options?.strict ?? false;
1525
1530
  const stripUnknown = options?.stripUnknown ?? false;
1531
+ const caseInsensitive = options?.caseInsensitive ?? false;
1526
1532
  const suffix = options?.typeSuffix ?? '';
1527
1533
  // Special case for the self-referential JSON Schema meta-schema type (e.g.
1528
1534
  // `Schema` / `SchemaObject`) - it can be any JSON Schema. This is an OpenAPI
@@ -1541,6 +1547,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1541
1547
  useRefImports,
1542
1548
  suffix,
1543
1549
  stripUnknown,
1550
+ caseInsensitive,
1544
1551
  logWarnings,
1545
1552
  ...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
1546
1553
  ...(options?.reservedNames !== undefined ? { reservedNames: options.reservedNames } : {}),
@@ -1550,24 +1557,24 @@ const selectParserStrategy = (schema, typeName, options) => {
1550
1557
  // This generates a parser that handles known properties and also iterates
1551
1558
  // pattern-matched keys (e.g. responses with "default" + "200", "4XX").
1552
1559
  if (hasProperties(schema) && isSchemaObject(schema) && 'patternProperties' in schema) {
1553
- return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown);
1560
+ return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive);
1554
1561
  }
1555
1562
  // Handle schemas that have explicit properties — generate a full object parser.
1556
1563
  // This intentionally runs before if/then checks so that schemas with both
1557
1564
  // properties AND conditional keywords use all declared properties rather than
1558
1565
  // only the if/then fragment.
1559
1566
  if (hasProperties(schema)) {
1560
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames);
1567
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive);
1561
1568
  }
1562
1569
  // Handle conditional schemas (if/then/else) for schemas without explicit properties.
1563
1570
  if (isSchemaObject(schema) && 'if' in schema && 'then' in schema && 'else' in schema) {
1564
- return generateConditionalParser(schema, typeName, suffix, stripUnknown);
1571
+ return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive);
1565
1572
  }
1566
1573
  // Handle conditional schemas that only define if/then object fragments.
1567
1574
  // We flatten the fragments into a regular object parser.
1568
1575
  const conditionalObjectSchema = getConditionalObjectSchema(schema);
1569
1576
  if (conditionalObjectSchema) {
1570
- return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
1577
+ return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, undefined, NO_RESERVED_NAMES, caseInsensitive);
1571
1578
  }
1572
1579
  // Handle non-object schemas with type-appropriate validation (no properties, no conditionals)
1573
1580
  if (!isObjectLikeSchema) {
@@ -1575,6 +1582,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1575
1582
  useRefImports,
1576
1583
  suffix,
1577
1584
  stripUnknown,
1585
+ caseInsensitive,
1578
1586
  logWarnings,
1579
1587
  ...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
1580
1588
  ...(options?.reservedNames !== undefined ? { reservedNames: options.reservedNames } : {}),
@@ -18,4 +18,4 @@ export declare const isCoercibleItemSchema: (itemSchema: JSONSchema) => boolean;
18
18
  * Returns the value if valid, coerces to the correct type if invalid, or uses default if required and missing.
19
19
  * Supports type checking, pattern validation, enum validation, union types, and $ref resolution.
20
20
  */
21
- export declare const generateValidationExpression: (key: string, schema: JSONSchema, defaultValue: string, isRequired: boolean, rootSchema?: Record<string, unknown>, visitedRefs?: Set<string>, accessorOverride?: string, knownNotUndefined?: boolean) => string;
21
+ export declare const generateValidationExpression: (key: string, schema: JSONSchema, defaultValue: string, isRequired: boolean, rootSchema?: Record<string, unknown>, visitedRefs?: Set<string>, accessorOverride?: string, knownNotUndefined?: boolean, caseInsensitive?: boolean) => string;
@@ -7,7 +7,7 @@ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExcl
7
7
  import { findDiscriminator } from '../helpers/find-discriminator.js';
8
8
  import { getDefaultValue } from '../helpers/get-default-value.js';
9
9
  import { generateDiscriminatedUnionValidation } from './generate-discriminated-union-validation.js';
10
- import { generateEnumCheck } from './generate-enum-check.js';
10
+ import { generateEnumCaseInsensitiveCoercion, generateEnumCheck } from './generate-enum-check.js';
11
11
  import { generateNonDiscriminatedUnionValidation } from './generate-non-discriminated-union-validation.js';
12
12
  import { generateSchemaChecks } from './generate-schema-checks.js';
13
13
  /**
@@ -128,7 +128,7 @@ const getTypeCoercion = (accessor, schema, defaultValue) => {
128
128
  * Returns the value if valid, coerces to the correct type if invalid, or uses default if required and missing.
129
129
  * Supports type checking, pattern validation, enum validation, union types, and $ref resolution.
130
130
  */
131
- export const generateValidationExpression = (key, schema, defaultValue, isRequired, rootSchema, visitedRefs, accessorOverride, knownNotUndefined) => {
131
+ export const generateValidationExpression = (key, schema, defaultValue, isRequired, rootSchema, visitedRefs, accessorOverride, knownNotUndefined, caseInsensitive) => {
132
132
  const accessor = accessorOverride ?? safeAccessor('input?', key);
133
133
  const checks = [];
134
134
  if (!isSchemaObject(schema)) {
@@ -170,7 +170,7 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
170
170
  if (resolvedSchema) {
171
171
  const visited = new Set(visitedRefs);
172
172
  visited.add(ref);
173
- return generateValidationExpression(key, resolvedSchema, defaultValue, isRequired, rootSchema, visited);
173
+ return generateValidationExpression(key, resolvedSchema, defaultValue, isRequired, rootSchema, visited, undefined, undefined, caseInsensitive);
174
174
  }
175
175
  }
176
176
  // Handle union types (oneOf, anyOf)
@@ -250,7 +250,7 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
250
250
  !Array.isArray(schema.items) &&
251
251
  isCoercibleItemSchema(schema.items)) {
252
252
  const itemSchema = schema.items;
253
- const itemExpr = generateValidationExpression('', itemSchema, getDefaultValue(itemSchema), true, rootSchema, visitedRefs, '_it', true);
253
+ const itemExpr = generateValidationExpression('', itemSchema, getDefaultValue(itemSchema), true, rootSchema, visitedRefs, '_it', true, caseInsensitive);
254
254
  const mapped = `(Array.isArray(${accessor}) ? (${accessor} as unknown[]).map((_it) => ${itemExpr}) : ${defaultValue})`;
255
255
  return isRequired ? mapped : `(${accessor} !== undefined ? ${mapped} : undefined)`;
256
256
  }
@@ -361,6 +361,14 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
361
361
  // enum. Fall back to the default (the first enum value) so the result is always
362
362
  // a valid member of the declared literal-union type.
363
363
  const typeCoercion = hasEnum(schema) ? null : getTypeCoercion(accessor, schema, defaultValue);
364
+ // Case-insensitive enum normalization is spliced into the *failure* branch of
365
+ // the ternary only, so an already-valid (exactly-cased) member takes the
366
+ // `${accessor}` fast branch untouched. A mis-cased string folds to the exact
367
+ // member; anything else falls through to the branch's normal fallback. `null`
368
+ // when the flag is off or no member is a string, so the plain fallback stands.
369
+ const enumFallback = (fallback) => (caseInsensitive && hasEnum(schema) && schema.enum.length > 0
370
+ ? generateEnumCaseInsensitiveCoercion(accessor, schema.enum, fallback)
371
+ : null) ?? fallback;
364
372
  if (isRequired) {
365
373
  // For required fields: valid ? use_value : (exists ? coerce : default)
366
374
  if (typeCoercion) {
@@ -371,7 +379,7 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
371
379
  }
372
380
  return `${combinedCheck} ? ${accessor} : (${accessor} !== undefined ? ${typeCoercion} : ${defaultValue})`;
373
381
  }
374
- return `${combinedCheck} ? ${accessor} : ${defaultValue}`;
382
+ return `${combinedCheck} ? ${accessor} : ${enumFallback(defaultValue)}`;
375
383
  }
376
384
  else {
377
385
  // For optional fields: valid ? use_value : (exists ? coerce : undefined)
@@ -382,6 +390,6 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
382
390
  }
383
391
  return `${combinedCheck} ? ${accessor} : (${accessor} !== undefined ? ${typeCoercion} : undefined)`;
384
392
  }
385
- return `${combinedCheck} ? ${accessor} : undefined`;
393
+ return `${combinedCheck} ? ${accessor} : ${enumFallback('undefined')}`;
386
394
  }
387
395
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",