@amritk/generate-parsers 0.19.4 → 0.20.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/AI.md CHANGED
@@ -26,7 +26,11 @@ const files = await buildSchema(schema, 'Document')
26
26
  To set a later flag you must pass every intervening positional. (The README's
27
27
  short form omits the trailing seven.)
28
28
  2. **It returns files in memory — it does NOT write to disk.** You write the
29
- `GeneratedFile[]` (`{ filename, content }`) yourself.
29
+ `GeneratedFile[]` (`{ filename, content }`) yourself. `rootTypeName` becomes
30
+ that filename (lowercased) as well as the `export type` name, so it must be a
31
+ TypeScript identifier — `buildSchema` throws otherwise rather than hand you a
32
+ path that escapes your output directory. Derive it with `@amritk/helpers`'
33
+ `deriveRootTypeName`, which always produces one.
30
34
  3. **Output is more than one file per `$def`:** always an `index.ts` barrel, and
31
35
  (unless `typesOnly`) runtime helper files.
32
36
  4. **Default parsers COERCE invalid input to defaults** rather than throwing;
package/README.md CHANGED
@@ -82,7 +82,7 @@ Every option after the first two is **positional** — pass them in this order:
82
82
  | # | Parameter | Type | Default | Description |
83
83
  |--:|:---|:---|:---|:---|
84
84
  | 1 | `rootSchema` | `JSONSchema` | — | The root schema to traverse. `$ref` and `$dynamicRef` are resolved recursively. |
85
- | 2 | `rootTypeName` | `string` | — | Name used for the root type (e.g. `"Document"`). |
85
+ | 2 | `rootTypeName` | `string` | — | Name used for the root type (e.g. `"Document"`). Must be a TypeScript identifier: it is emitted verbatim as `export type <name>` and, lowercased, becomes the output filename, so a name derived from an untrusted document is refused rather than written outside your output directory. `@amritk/helpers`' `deriveRootTypeName` always produces one. |
86
86
  | 3 | `extensions` | `SchemaExtensions` | — | Map of definition name → extra optional properties to merge in before generation. |
87
87
  | 4 | `typesOnly` | `boolean` | — | Only emit `.ts` type definitions — skip parser functions and runtime helpers. |
88
88
  | 5 | `logWarnings` | `boolean` | — | Generated parsers emit a `console.warn` for every input key not declared in the schema's properties. |
@@ -296,23 +296,32 @@ keys) and `parseStrict` (assert + reject undeclared keys) halves of
296
296
  [`moltar/typescript-runtime-type-benchmarks`](https://github.com/moltar/typescript-runtime-type-benchmarks)
297
297
  against the other *pure* parsers — the ones that return a new typed value rather
298
298
  than mutating in place — [zod](https://zod.dev) and
299
- [TypeBox](https://github.com/sinclairzx81/typebox). Measured on Bun 1.3 (Linux
299
+ [TypeBox](https://github.com/sinclairzx81/typebox). Measured on Bun 1.4 (Linux
300
300
  x64), parsing valid input at steady state:
301
301
 
302
302
  | case | mode | mjst (generated) | zod | typebox |
303
303
  |:--|:--|--:|--:|--:|
304
- | user (4 fields) | parseSafe | **~18M** ops/s | ~3M ops/s | ~1.6M ops/s |
305
- | order (nested + array) | parseSafe | **~7.2M** ops/s | ~0.59M ops/s | ~0.18M ops/s |
306
- | user (4 fields) | parseStrict | **~15M** ops/s | ~2M ops/s | ~2.05M ops/s |
307
- | order (nested + array) | parseStrict | **~8.5M** ops/s | ~0.38M ops/s | ~0.29M ops/s |
304
+ | user (4 fields) | parseSafe | **~18M** ops/s | ~3.7M ops/s | ~1.6M ops/s |
305
+ | order (nested + array) | parseSafe | **~6.9M** ops/s | ~0.71M ops/s | ~0.24M ops/s |
306
+ | user (4 fields) | parseStrict | **~14M** ops/s | ~2.3M ops/s | ~2.04M ops/s |
307
+ | order (nested + array) | parseStrict | **~7M** ops/s | ~0.43M ops/s | ~0.36M ops/s |
308
308
 
309
309
  The upstream `assert` case (seven scalar roots plus a nested object) runs faster
310
- still — ~44M ops/s strict, ~97M safe — but at that size the numbers swing enough
310
+ still — ~51M ops/s strict, ~127M safe — but at that size the numbers swing enough
311
311
  run-to-run that the ratio, not the absolute, is the honest signal: mjst lands
312
- ~6–30× ahead of zod across every case above.
312
+ ~5–29× ahead of zod across every case above.
313
+
314
+ What is replicated upstream is the *cases* — the two parse modes and their
315
+ shapes — not the harness. These numbers come from this repo's own measurement
316
+ core (direct calls over a pool of distinct inputs, median of many trials) and
317
+ are not comparable with the public leaderboard, which measures every operation
318
+ through benny and reports something an order of magnitude smaller for the same
319
+ function. `@amritk/generate-validators` carries the side-by-side and a
320
+ reproducible run of both harnesses:
321
+ [Against the moltar harness](../generate-validators#against-the-moltar-harness).
313
322
 
314
323
  The trade is a one-shot **prepare** cost that only mjst pays — generating the
315
- parser source — which measures **~0.15–1 ms** per schema here (zod and TypeBox
324
+ parser source — which measures **~0.2–0.8 ms** per schema here (zod and TypeBox
316
325
  author or interpret their parsers with no separate build step, so there is
317
326
  nothing to time). That is trivially amortized: you generate once at build time
318
327
  and run the emitted code forever.
@@ -19,7 +19,10 @@ export type GeneratedFile = {
19
19
  * TypeScript file, then emits an `index.ts` barrel re-exporting them all.
20
20
  *
21
21
  * @param rootSchema - The root JSON Schema to build from
22
- * @param rootTypeName - The name for the root type (e.g., "Document")
22
+ * @param rootTypeName - The name for the root type (e.g., "Document"). Must be a TypeScript
23
+ * identifier: it is emitted verbatim as `export type <name>` and, lowercased, becomes the
24
+ * output filename, so a name derived from an untrusted document is refused rather than
25
+ * written outside the output directory.
23
26
  * @param extensions - Optional map of custom extension properties to add to specific definitions.
24
27
  * Keys are definition names (matching $defs keys), values are records of extension property
25
28
  * names to their JSON Schema definitions. Extensions are merged as optional properties before
@@ -20,7 +20,14 @@ const readHelperSource = async (helper, importExt) => {
20
20
  return { ext: "js", content: compiled };
21
21
  }
22
22
  };
23
+ const ROOT_TYPE_NAME = /^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u;
24
+ const assertRootTypeName = (rootTypeName) => {
25
+ if (ROOT_TYPE_NAME.test(rootTypeName))
26
+ return;
27
+ throw new Error(`Invalid root type name ${JSON.stringify(rootTypeName)}: expected a TypeScript identifier. The name is emitted as \`export type <name>\` and, lowercased, used as the output filename, so anything else either fails to compile or escapes the output directory. Derive it with @amritk/helpers' \`deriveRootTypeName\`, which always produces one.`);
28
+ };
23
29
  const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false, stripUnknown = false, typeSuffix = "", importExt = "js", caseInsensitive = false, schemas) => {
30
+ assertRootTypeName(rootTypeName);
24
31
  const files = [];
25
32
  const usedHelpers = /* @__PURE__ */ new Set();
26
33
  const foldedSchema = foldNullable(rootSchema);
@@ -151,7 +151,25 @@ const generateFallbackValue = (_, propSchema, useRefImports, suffix) => {
151
151
  return getDefaultValue(propSchema);
152
152
  };
153
153
  const PROTOTYPE_MEMBER_NAMES = new Set(Object.getOwnPropertyNames(Object.prototype));
154
- const declaresPrototypeMemberProperty = (schema) => hasProperties(schema) && Object.keys(schema.properties).some((key) => PROTOTYPE_MEMBER_NAMES.has(key));
154
+ const declaresPrototypeMemberProperty = (schema, depth = 4) => {
155
+ if (!isSchemaObject(schema) || depth < 0)
156
+ return false;
157
+ if (hasProperties(schema) && Object.keys(schema.properties).some((key) => PROTOTYPE_MEMBER_NAMES.has(key)))
158
+ return true;
159
+ const record = schema;
160
+ const children = [];
161
+ if (hasProperties(schema))
162
+ children.push(...Object.values(schema.properties));
163
+ const items = record["items"];
164
+ if (Array.isArray(items))
165
+ children.push(...items);
166
+ else if (items !== void 0)
167
+ children.push(items);
168
+ const prefixItems = record["prefixItems"];
169
+ if (Array.isArray(prefixItems))
170
+ children.push(...prefixItems);
171
+ return children.some((child) => declaresPrototypeMemberProperty(child, depth - 1));
172
+ };
155
173
  const generateFallbackObject = (schema, useRefImports, typeName, suffix, subTypeNames) => {
156
174
  if (!hasProperties(schema)) {
157
175
  return `{} as ${typeName}`;
@@ -183,33 +201,11 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
183
201
  }
184
202
  }
185
203
  result += "\n }";
186
- return declaresPrototypeMemberProperty(schema) ? `${result} as ${typeName}` : result;
204
+ return declaresPrototypeMemberProperty(schema) ? `${result} as unknown as ${typeName}` : result;
187
205
  };
188
206
  const scalarDefaultLiteral = (schema) => {
189
- if (!isSchemaObject(schema))
190
- return "{}";
191
- if (hasConst(schema))
192
- return JSON.stringify(schema.const);
193
- if (hasEnum(schema) && schema.enum.length > 0)
194
- return JSON.stringify(schema.enum[0]);
195
- if (hasType(schema)) {
196
- switch (schema.type) {
197
- case "string":
198
- return '""';
199
- case "number":
200
- case "integer":
201
- return "0";
202
- case "boolean":
203
- return "false";
204
- case "null":
205
- return "null";
206
- case "array":
207
- return "[]";
208
- case "object":
209
- return "{}";
210
- }
211
- }
212
- return "{}";
207
+ const value = getDefaultValue(schema);
208
+ return value === "undefined" ? "{}" : value;
213
209
  };
214
210
  const generateRefUnionDispatch = (functionName, typeName, schema, branches, strict, ctx) => {
215
211
  const rootSchema = ctx.rootSchema;
@@ -552,9 +548,57 @@ const exactKeyCountOf = (schema) => {
552
548
  return null;
553
549
  return declaredCount;
554
550
  };
555
- const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
551
+ const shouldCacheVariable = (propSchema) => {
556
552
  return isSchemaObject(propSchema);
557
553
  };
554
+ const INLINE_NESTED_MAX_PROPERTIES = 12;
555
+ const INLINE_NESTED_MAX_SITES = 4;
556
+ const INLINE_NESTED_MAX_TOTAL_PROPERTIES = 24;
557
+ const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImports, suffix, stripUnknown, reservedNames) => {
558
+ if (!isSchemaObject(childSchema) || !hasProperties(childSchema))
559
+ return null;
560
+ if (!stripUnknown)
561
+ return null;
562
+ if (hasAdditionalProperties(childSchema) && childSchema.additionalProperties === false)
563
+ return null;
564
+ if (hasAllOf(childSchema) || hasOneOf(childSchema) || hasAnyOf(childSchema))
565
+ return null;
566
+ if (hasFastPathBlockingKeyword(childSchema))
567
+ return null;
568
+ if (hasMinProperties(childSchema) || hasMaxProperties(childSchema))
569
+ return null;
570
+ if (exactKeyCountOf(childSchema) === null)
571
+ return null;
572
+ const { objects, arrayItems } = collectInlineSubTypes(childSchema, childTypeName, reservedNames);
573
+ if (objects.size > 0 || arrayItems.size > 0)
574
+ return null;
575
+ const props = childSchema.properties;
576
+ const keys = Object.keys(props);
577
+ if (keys.length === 0 || keys.length > INLINE_NESTED_MAX_PROPERTIES)
578
+ return null;
579
+ const checks = [];
580
+ const fields = [];
581
+ const readRoot = `(${objectExpr} as Record<string, any>)`;
582
+ for (const key of keys) {
583
+ const propSchema = props[key];
584
+ if (shouldUseRefImport(propSchema, useRefImports) || shouldUseArrayRefImport(propSchema, useRefImports) || shouldUseRecordRefImport(propSchema, useRefImports)) {
585
+ return null;
586
+ }
587
+ const accessor = safeAccessor(readRoot, key);
588
+ const check = generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
589
+ if (check === null)
590
+ return null;
591
+ checks.push(check);
592
+ fields.push(`${safeLiteralKey(key)}: ${accessor}`);
593
+ }
594
+ const condition = [
595
+ `typeof ${objectExpr} === "object"`,
596
+ `${objectExpr} !== null`,
597
+ `!Array.isArray(${objectExpr})`,
598
+ ...checks
599
+ ].join(" && ");
600
+ return { condition, literal: `{ ${fields.join(", ")} }`, propertyCount: keys.length };
601
+ };
558
602
  const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource) => {
559
603
  const functionName = generateParserName(typeName);
560
604
  const exportPrefix = exported ? "export " : "";
@@ -592,9 +636,15 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
592
636
  }
593
637
  const strictKeys = hasStrictKeys(schema);
594
638
  const stripKeys = strictKeys || stripUnknown;
595
- const exactKeyCount = stripKeys ? exactKeyCountOf(schema) : null;
639
+ const rejectsUnknownKeys = strict && isSchemaObject(schema) && hasAdditionalProperties(schema) && schema.additionalProperties === false && !("patternProperties" in schema) || false;
640
+ const guardKeys = stripKeys || rejectsUnknownKeys;
641
+ const fastPathBuildsDeclaredLiteral = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema));
642
+ const fastPathNeedsKnownKeys = guardKeys && (rejectsUnknownKeys || !fastPathBuildsDeclaredLiteral);
643
+ const droppedKnownKeysTerm = guardKeys && !fastPathNeedsKnownKeys;
644
+ const exactKeyCount = guardKeys ? exactKeyCountOf(schema) : null;
596
645
  const strictKeyCheck = unknownKeyCheck(propertyKeys, `_knownKeys${typeName}`);
597
- if (stripKeys && exactKeyCount === null) {
646
+ const shapeValidatorNeedsKnownKeys = shapeValidatorSource?.includes(`_hasOnlyKnownKeys${typeName}(`) ?? false;
647
+ if ((fastPathNeedsKnownKeys || shapeValidatorNeedsKnownKeys) && exactKeyCount === null) {
598
648
  for (const declaration of strictKeyCheck.declarations) {
599
649
  preamble.push(`${declaration};`);
600
650
  }
@@ -602,7 +652,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
602
652
  for (const _k in input) if (${strictKeyCheck.isUnknown("_k")}) return false;
603
653
  return true;
604
654
  };`);
605
- } else if (strict && strictKeys) {
655
+ } else if (rejectsUnknownKeys) {
606
656
  for (const declaration of strictKeyCheck.declarations) {
607
657
  preamble.push(`${declaration};`);
608
658
  }
@@ -661,13 +711,13 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
661
711
  fastPathAccessorChecks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
662
712
  }
663
713
  }
664
- if (stripKeys) {
714
+ if (fastPathNeedsKnownKeys) {
665
715
  fastPathChecks.push(exactKeyCount !== null ? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}` : `_hasOnlyKnownKeys${typeName}(input)`);
666
716
  }
667
717
  const deepGuard = canFastPath && fastPathChecks.length > 0 ? fastPathChecks.join(" && ") : null;
668
718
  const literalReturnUsesVars = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema)) && exported;
669
719
  let deepGuardExpr = deepGuard;
670
- if (deepGuard !== null && shapeValidatorSource !== void 0 && !literalReturnUsesVars) {
720
+ if (deepGuard !== null && shapeValidatorSource !== void 0 && !literalReturnUsesVars && !droppedKnownKeysTerm) {
671
721
  const shapeFnName = shapeValidatorName(typeName);
672
722
  const shapeStrictKeysGuard = stripKeys && exactKeyCount === null ? `
673
723
  if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : "";
@@ -710,7 +760,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
710
760
  const shallowGuard = canShallowGuard && shallowChecks.length > 0 ? shallowChecks.join(" && ") : null;
711
761
  const varDeclLines = [];
712
762
  for (const { key, varName, propSchema } of propInfo) {
713
- if (shouldCacheVariable(propSchema, canFastPath, useRefImports)) {
763
+ if (shouldCacheVariable(propSchema)) {
714
764
  varDeclLines.push(` const ${varName} = ${safeAccessor("input", key)};`);
715
765
  }
716
766
  }
@@ -728,6 +778,8 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
728
778
  }
729
779
  const buildObjectLines = (directAssign) => {
730
780
  const objectLines = [];
781
+ let inlinedSites = 0;
782
+ let inlinedProperties = 0;
731
783
  if (!stripKeys) {
732
784
  objectLines.push(" ...input,");
733
785
  }
@@ -740,12 +792,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
740
792
  }
741
793
  }
742
794
  for (const { key, varName, isRequired, propSchema } of propInfo) {
743
- const shouldCache = shouldCacheVariable(propSchema, canFastPath, useRefImports);
795
+ const shouldCache = shouldCacheVariable(propSchema);
744
796
  const accessor = shouldCache ? varName : safeAccessor("input", key);
745
797
  const subName = subTypeNames.get(key);
746
798
  if (subName) {
747
799
  const subParserName = generateParserName(subName);
748
800
  if (isRequired) {
801
+ const inlined = directAssign && !logWarnings && inlinedSites < INLINE_NESTED_MAX_SITES ? inlineNestedFastPath(propSchema, subName, accessor, useRefImports, suffix, stripUnknown, reservedNames) : null;
802
+ if (inlined !== null && inlinedProperties + inlined.propertyCount <= INLINE_NESTED_MAX_TOTAL_PROPERTIES) {
803
+ inlinedSites++;
804
+ inlinedProperties += inlined.propertyCount;
805
+ objectLines.push(` ${safeLiteralKey(key)}: (${inlined.condition})
806
+ ? ${inlined.literal}
807
+ : ${subParserName}(${accessor}),`);
808
+ continue;
809
+ }
749
810
  objectLines.push(` ${safeLiteralKey(key)}: ${subParserName}(${accessor}),`);
750
811
  } else {
751
812
  objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${subParserName}(${accessor}) }),`);
@@ -828,12 +889,11 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
828
889
  lines2.push(` } as unknown as ${typeName};`);
829
890
  };
830
891
  const emitDeepGuardReturn = (lines2) => {
831
- const canLiteral = stripKeys && !(isSchemaObject(schema) && hasAllOf(schema));
832
- if (!canLiteral) {
892
+ if (!fastPathBuildsDeclaredLiteral) {
833
893
  lines2.push(` if (${deepGuardExpr}) return { ...input } as ${typeName};`);
834
894
  return;
835
895
  }
836
- if (!exported) {
896
+ if (!exported && fastPathNeedsKnownKeys) {
837
897
  lines2.push(` if (${deepGuardExpr}) return input as ${typeName};`);
838
898
  return;
839
899
  }
@@ -848,12 +908,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
848
908
  }
849
909
  continue;
850
910
  }
851
- const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports) ? varName : safeAccessor("input", key);
911
+ const accessor = shouldCacheVariable(propSchema) ? varName : safeAccessor("input", key);
852
912
  fields.push(isRequired ? ` ${safeLiteralKey(key)}: ${accessor},` : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
853
913
  }
854
914
  lines2.push(` if (${deepGuardExpr}) return {`);
855
915
  lines2.push(fields.join("\n"));
856
- lines2.push(` } as ${typeName};`);
916
+ lines2.push(` } as unknown as ${typeName};`);
857
917
  };
858
918
  const lines = [];
859
919
  lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
@@ -870,7 +930,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
870
930
  ...rootSchema !== void 0 ? { rootSchema } : {}
871
931
  }).slice(1);
872
932
  if (shallowGuard) {
873
- if (!exported && deepGuard) {
933
+ if (!exported && deepGuard && fastPathNeedsKnownKeys) {
874
934
  const residual = [];
875
935
  for (let i = 0; i < fastPathChecks.length; i++) {
876
936
  const deep = fastPathChecks[i];
@@ -903,7 +963,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
903
963
  for (const assertionLine of assertionLines) {
904
964
  lines.push(assertionLine);
905
965
  }
906
- if (strictKeys) {
966
+ if (rejectsUnknownKeys) {
907
967
  lines.push(` for (const _k in input) {`);
908
968
  lines.push(` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
909
969
  lines.push(` }`);
@@ -1280,6 +1340,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1280
1340
  stripUnknown,
1281
1341
  ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {}
1282
1342
  };
1343
+ const copyLoopContext = { ...context, subParsers: false };
1283
1344
  if (!options?.isRoot && typeName === `Schema${suffix}`) {
1284
1345
  return generateSchemaObjectParser(typeName);
1285
1346
  }
@@ -1296,7 +1357,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1296
1357
  });
1297
1358
  }
1298
1359
  if (hasProperties(schema) && isSchemaObject(schema) && "patternProperties" in schema) {
1299
- return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive, context);
1360
+ return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive, copyLoopContext);
1300
1361
  }
1301
1362
  if (strict && isSchemaObject(schema) && Array.isArray(schema.type) && schema.type.includes("null")) {
1302
1363
  return generateNonObjectParser(typeName, schema, strict, {
@@ -1342,14 +1403,14 @@ const selectParserStrategy = (schema, typeName, options) => {
1342
1403
  });
1343
1404
  }
1344
1405
  if ("patternProperties" in schema) {
1345
- return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict, context);
1406
+ return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict, copyLoopContext);
1346
1407
  }
1347
1408
  if ("additionalProperties" in schema && !hasProperties(schema)) {
1348
1409
  const additionalProps = schema.additionalProperties;
1349
1410
  if (additionalProps === true || additionalProps === false) {
1350
1411
  return generateEmptyObjectParser(typeName, strict, schema, context);
1351
1412
  }
1352
- return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict, context);
1413
+ return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict, copyLoopContext);
1353
1414
  }
1354
1415
  if ("type" in schema && schema.type === "object" && !hasProperties(schema)) {
1355
1416
  return generateEmptyObjectParser(typeName, strict, schema, context);
@@ -3,6 +3,8 @@ import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extens
3
3
  import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
4
4
  import { hasOwnCheck } from "@amritk/helpers/safe-accessor";
5
5
  import { hasAdditionalProperties, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasPattern, hasProperties, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
6
+ import { maxLengthPassExpr, minLengthPassExpr } from "@amritk/helpers/string-length-check";
7
+ import { generateDeepEqualCheck } from "./generate-deep-equal-check.js";
6
8
  import { generateEnumCheck } from "./generate-enum-check.js";
7
9
  import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
8
10
  const OBJECT_KEYWORDS = [
@@ -95,10 +97,10 @@ const generateInferredChecks = (accessor, schema, type) => {
95
97
  checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
96
98
  }
97
99
  if (hasMinLength(schema)) {
98
- checks.push(`${accessor}.length >= ${schema.minLength}`);
100
+ checks.push(minLengthPassExpr(accessor, schema.minLength));
99
101
  }
100
102
  if (hasMaxLength(schema)) {
101
- checks.push(`${accessor}.length <= ${schema.maxLength}`);
103
+ checks.push(maxLengthPassExpr(accessor, schema.maxLength));
102
104
  }
103
105
  break;
104
106
  case "number":
@@ -140,6 +142,23 @@ const generateSchemaChecks = (accessor, schema) => {
140
142
  if (primitive) {
141
143
  return [`typeof ${accessor} === "${primitive}"`];
142
144
  }
145
+ if (hasConst(schema)) {
146
+ return [generateDeepEqualCheck(accessor, schema.const)];
147
+ }
148
+ if (isSchemaObject(schema) && Array.isArray(schema.type)) {
149
+ const branches = [];
150
+ for (const type of schema.type) {
151
+ if (typeof type !== "string")
152
+ continue;
153
+ const branch = generateSchemaChecks(accessor, { ...schema, type });
154
+ if (branch.length === 0)
155
+ continue;
156
+ branches.push(branch.length === 1 ? branch[0] : `(${branch.join(" && ")})`);
157
+ }
158
+ if (branches.length === 0)
159
+ return checks;
160
+ return [branches.length === 1 ? branches[0] : `(${branches.join(" || ")})`];
161
+ }
143
162
  if (!hasType(schema)) {
144
163
  const inferred = inferSchemaType(schema);
145
164
  if (!inferred) {
@@ -158,10 +177,10 @@ const generateSchemaChecks = (accessor, schema) => {
158
177
  checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
159
178
  }
160
179
  if (hasMinLength(schema)) {
161
- checks.push(`${accessor}.length >= ${schema.minLength}`);
180
+ checks.push(minLengthPassExpr(accessor, schema.minLength));
162
181
  }
163
182
  if (hasMaxLength(schema)) {
164
- checks.push(`${accessor}.length <= ${schema.maxLength}`);
183
+ checks.push(maxLengthPassExpr(accessor, schema.maxLength));
165
184
  }
166
185
  break;
167
186
  }
@@ -190,6 +209,13 @@ const generateSchemaChecks = (accessor, schema) => {
190
209
  case "boolean":
191
210
  checks.push(`typeof ${accessor} === "boolean"`);
192
211
  break;
212
+ // Without this the `null` branch of a union produced NO check, which reads
213
+ // as "this branch matches nothing": `{ anyOf: [{ type: 'string' },
214
+ // { type: 'null' }] }` rejected `null` — the one value the branch exists to
215
+ // admit — and the coercing build dropped the property instead of keeping it.
216
+ case "null":
217
+ checks.push(`${accessor} === null`);
218
+ break;
193
219
  case "array":
194
220
  checks.push(`Array.isArray(${accessor})`);
195
221
  if (hasMinItems(schema)) {
@@ -13,6 +13,17 @@ export type StrictAssertionContext = {
13
13
  readonly suffix?: string;
14
14
  readonly rootSchema?: Record<string, unknown>;
15
15
  readonly stripUnknown?: boolean;
16
+ /**
17
+ * Whether the parser these assertions run inside also emits the private
18
+ * sub-parsers for its inline object properties (and inline-object array
19
+ * items). `generateObjectParser` does, so the shape of such a property is
20
+ * proven by the call it delegates to and asserting it here would be
21
+ * redundant. The pattern-property parsers do NOT — they build their result
22
+ * with a copy loop — so a nested object property there is proven by nobody
23
+ * unless these assertions prove it. Defaults to true, the historical
24
+ * assumption.
25
+ */
26
+ readonly subParsers?: boolean;
16
27
  };
17
28
  /**
18
29
  * Why a node needs the whole-schema backstop, or `null` when the specialized
@@ -10,8 +10,9 @@ import { generateDeepEqualCheck } from "./generate-deep-equal-check.js";
10
10
  import { generateEnumCheck } from "./generate-enum-check.js";
11
11
  import { canEnforceUnion, generateUnionCheck, getUnionBranches, isExclusiveUnion, isInlineObjectProperty, multiTypeCheck } from "./generate-type-checks.js";
12
12
  import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
13
- import { everyTailItem, getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from "./generate-validation-expression.js";
13
+ import { everyTailItem, getPrefixItems, plainScalarItemCheck, prefixItemsCapsLength } from "./generate-validation-expression.js";
14
14
  import { hasConstrainingRefSibling, subschemaMatchExpr } from "./subschema-match.js";
15
+ const shapeIsDelegated = (schema, context) => (context.subParsers ?? true) && isInlineObjectProperty(schema);
15
16
  const matchContext = (context) => context.rootSchema ? { rootSchema: context.rootSchema } : {};
16
17
  const TYPELESS_CONSTRAINT_KEYWORDS = [
17
18
  "minimum",
@@ -212,11 +213,11 @@ const generateItemCheck = (schema, context) => {
212
213
  const label = items.enum.map((v) => JSON.stringify(v)).join(", ");
213
214
  return { check: generateEnumCheck("_it", items.enum), message: `items must be one of: ${label}` };
214
215
  }
215
- const scalarCheck = scalarItemTypeCheck(items, "_it");
216
+ const scalarCheck = plainScalarItemCheck(items, "_it");
216
217
  if (scalarCheck !== null && isSchemaObject(items) && hasType(items)) {
217
218
  return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
218
219
  }
219
- if (hasRef(items) && context.useRefImports === true || isInlineObjectProperty(items))
220
+ if (hasRef(items) && context.useRefImports === true || shapeIsDelegated(items, context))
220
221
  return null;
221
222
  const match = subschemaMatchExpr("_it", items, matchContext(context));
222
223
  if (match === null || match === "true")
@@ -232,6 +233,34 @@ const nullableObjectShapeChecks = (acc, schema, type, label, optional, context)
232
233
  const presence = optional ? `${acc} !== undefined && ${acc} !== null` : `${acc} !== null`;
233
234
  return [` if (${presence} && !(${match})) ${throwError(`${label} does not match the declared object shape`)};`];
234
235
  };
236
+ const objectShapeResidual = (schema, context) => {
237
+ if (!isSchemaObject(schema) || !hasType(schema) || schema.type !== "object")
238
+ return null;
239
+ if (shapeIsDelegated(schema, context))
240
+ return null;
241
+ const record = schema;
242
+ const residual = { type: "object" };
243
+ if (hasProperties(schema))
244
+ residual["properties"] = record["properties"];
245
+ if (hasRequired(schema))
246
+ residual["required"] = record["required"];
247
+ if (record["additionalProperties"] === false) {
248
+ residual["additionalProperties"] = false;
249
+ if ("patternProperties" in record)
250
+ residual["patternProperties"] = record["patternProperties"];
251
+ }
252
+ return "properties" in residual || "required" in residual || "additionalProperties" in residual ? residual : null;
253
+ };
254
+ const declaredObjectShapeChecks = (acc, schema, label, optional, context) => {
255
+ const residual = objectShapeResidual(schema, context);
256
+ if (residual === null)
257
+ return [];
258
+ const match = subschemaMatchExpr(acc, residual, matchContext(context));
259
+ if (match === null || match === "true")
260
+ return [];
261
+ const presence = optional ? `${acc} !== undefined && ` : "";
262
+ return [` if (${presence}!(${match})) ${throwError(`${label} does not match the declared object shape`)};`];
263
+ };
235
264
  const generateNotCheck = (acc, schema, label, optional, context) => {
236
265
  if (!isSchemaObject(schema) || !("not" in schema))
237
266
  return [];
@@ -518,6 +547,7 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
518
547
  lines.push(...generateAllOfChecks(acc, propSchema, field, !isRequired, context));
519
548
  lines.push(...generateUnionMatchChecks(acc, propSchema, field, !isRequired, context));
520
549
  lines.push(...generateConditionalChecks(acc, propSchema, field, context));
550
+ lines.push(...declaredObjectShapeChecks(acc, propSchema, field, !isRequired, context));
521
551
  const unionBranches = getUnionBranches(propSchema);
522
552
  if (unionBranches) {
523
553
  if (!context.stripUnknown && canEnforceUnion(unionBranches, context.rootSchema)) {
@@ -3,10 +3,11 @@ import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
3
3
  import { refToName } from "@amritk/helpers/ref-to-name";
4
4
  import { resolveRef } from "@amritk/helpers/resolve-ref";
5
5
  import { hasOwnCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
6
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
6
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
7
+ import { maxLengthPassExpr, minLengthPassExpr } from "@amritk/helpers/string-length-check";
7
8
  import { generateEnumCheck } from "./generate-enum-check.js";
8
9
  import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
9
- import { everyTailItem, getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck, singleTypeCheck } from "./generate-validation-expression.js";
10
+ import { everyTailItem, getPrefixItems, plainScalarItemCheck, prefixItemsCapsLength, singleTypeCheck } from "./generate-validation-expression.js";
10
11
  import { hasConstrainingRefSibling, subschemaMatchExpr } from "./subschema-match.js";
11
12
  const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
12
13
  const isInlineObjectProperty = (propSchema) => {
@@ -92,6 +93,10 @@ const CONSTRAINT_KEYWORDS = [
92
93
  "minProperties",
93
94
  "maxProperties"
94
95
  ];
96
+ const hasBranchSiblingConstraint = (schema) => {
97
+ const record = schema;
98
+ return "type" in record || CONSTRAINT_KEYWORDS.some((keyword) => keyword in record);
99
+ };
95
100
  const multiTypeCheck = (varName, schema, options = {}) => {
96
101
  if (!isSchemaObject(schema) || !Array.isArray(schema.type))
97
102
  return void 0;
@@ -125,6 +130,8 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
125
130
  }
126
131
  const branches = getUnionBranches(schema);
127
132
  if (branches) {
133
+ if (hasBranchSiblingConstraint(schema))
134
+ return null;
128
135
  return generateUnionCheck(varName, branches, useRefImports, suffix, isExclusiveUnion(schema));
129
136
  }
130
137
  if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema) {
@@ -150,9 +157,9 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
150
157
  if (hasPattern(schema))
151
158
  checks.push(`${regexLiteral(schema.pattern)}.test(${varName})`);
152
159
  if (hasMinLength(schema))
153
- checks.push(`${varName}.length >= ${schema.minLength}`);
160
+ checks.push(minLengthPassExpr(varName, schema.minLength));
154
161
  if (hasMaxLength(schema))
155
- checks.push(`${varName}.length <= ${schema.maxLength}`);
162
+ checks.push(maxLengthPassExpr(varName, schema.maxLength));
156
163
  break;
157
164
  }
158
165
  case "number":
@@ -186,7 +193,7 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
186
193
  }
187
194
  if (hasItems(schema) && !Array.isArray(schema.items)) {
188
195
  const items = schema.items;
189
- const itemCheck = scalarItemTypeCheck(items, "_it") ?? (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0 ? generateEnumCheck("_it", items.enum) : null);
196
+ const itemCheck = plainScalarItemCheck(items, "_it") ?? (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0 ? generateEnumCheck("_it", items.enum) : null);
190
197
  if (itemCheck) {
191
198
  checks.push(everyTailItem(varName, itemCheck, schema));
192
199
  } else if (!(isSchemaObject(items) && (hasRef(items) || isInlineObjectProperty(items)))) {
@@ -211,6 +218,8 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
211
218
  break;
212
219
  }
213
220
  case "object":
221
+ if (hasProperties(schema) || hasRequired(schema))
222
+ return null;
214
223
  checks.push(`isObject(${varName})`);
215
224
  if (hasMinProperties(schema))
216
225
  checks.push(`Object.keys(${varName}).length >= ${schema.minProperties}`);
@@ -6,6 +6,16 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
6
6
  * objects, arrays, unions, `$ref`s, and anything richer.
7
7
  */
8
8
  export declare const scalarItemTypeCheck: (itemSchema: JSONSchema, accessor: string) => string | null;
9
+ /**
10
+ * {@link scalarItemTypeCheck}, but only for an item schema whose `type` is the
11
+ * *whole* constraint — the one case where the bare `typeof` test is an exact
12
+ * match rather than a weakening of it. Emit sites that need a check they can
13
+ * trust in both directions (a true-sound fast-path guard, a strict assertion
14
+ * that throws) take this one; the coercion path keeps the looser
15
+ * {@link isCoercibleItemSchema}, which coerces each element through the full
16
+ * per-element expression and so applies the constraints anyway.
17
+ */
18
+ export declare const plainScalarItemCheck: (itemSchema: JSONSchema, accessor: string) => string | null;
9
19
  /**
10
20
  * True when an array's `items` schema can be coerced element-by-element by the
11
21
  * mapping slow path: single scalar types (via {@link scalarItemTypeCheck}) and
@@ -4,6 +4,7 @@ import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
4
4
  import { resolveRef } from "@amritk/helpers/resolve-ref";
5
5
  import { hasOwnCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
6
6
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
7
+ import { maxLengthPassExpr, minLengthPassExpr } from "@amritk/helpers/string-length-check";
7
8
  import { findDiscriminator } from "../helpers/find-discriminator.js";
8
9
  import { getDefaultValue } from "../helpers/get-default-value.js";
9
10
  import { generateDiscriminatedUnionValidation } from "./generate-discriminated-union-validation.js";
@@ -34,10 +35,43 @@ const scalarItemTypeCheck = (itemSchema, accessor) => {
34
35
  return null;
35
36
  }
36
37
  };
38
+ const SCALAR_ITEM_CONSTRAINTS = [
39
+ "pattern",
40
+ "minLength",
41
+ "maxLength",
42
+ "minimum",
43
+ "maximum",
44
+ "exclusiveMinimum",
45
+ "exclusiveMaximum",
46
+ "multipleOf",
47
+ "const",
48
+ "$ref",
49
+ "not",
50
+ "allOf",
51
+ "anyOf",
52
+ "oneOf",
53
+ "if"
54
+ ];
55
+ const plainScalarItemCheck = (itemSchema, accessor) => {
56
+ if (!isSchemaObject(itemSchema))
57
+ return null;
58
+ const record = itemSchema;
59
+ if (SCALAR_ITEM_CONSTRAINTS.some((keyword) => keyword in record))
60
+ return null;
61
+ return scalarItemTypeCheck(itemSchema, accessor);
62
+ };
37
63
  const isCoercibleItemSchema = (itemSchema) => {
38
64
  if (scalarItemTypeCheck(itemSchema, "_it") !== null)
39
65
  return true;
40
- return isSchemaObject(itemSchema) && hasEnum(itemSchema) && itemSchema.enum.length > 0;
66
+ if (!isSchemaObject(itemSchema))
67
+ return false;
68
+ const itemTypes = itemSchema["type"];
69
+ if (Array.isArray(itemTypes) && itemTypes.length > 0) {
70
+ return itemTypes.every((type) => typeof type === "string" && scalarItemTypeCheck({ type }, "_it") !== null);
71
+ }
72
+ if (hasConst(itemSchema))
73
+ return true;
74
+ return hasEnum(itemSchema) && itemSchema.enum.length > 0;
41
75
  };
42
76
  const getPrefixItems = (schema) => {
43
77
  if (!isSchemaObject(schema))
@@ -50,7 +84,7 @@ const getPrefixItems = (schema) => {
50
84
  const everyTailItem = (accessor, itemCheck, schema) => {
51
85
  const prefix = isSchemaObject(schema) ? schema["prefixItems"] : void 0;
52
86
  const tailStart = Array.isArray(prefix) ? prefix.length : 0;
53
- return tailStart === 0 ? `${accessor}.every((_it) => ${itemCheck})` : `${accessor}.every((_it, _ix) => _ix < ${tailStart} || (${itemCheck}))`;
87
+ return tailStart === 0 ? `${accessor}.every((_it: unknown) => ${itemCheck})` : `${accessor}.every((_it: unknown, _ix: number) => _ix < ${tailStart} || (${itemCheck}))`;
54
88
  };
55
89
  const prefixItemsCapsLength = (schema) => {
56
90
  if (!isSchemaObject(schema))
@@ -83,25 +117,55 @@ const getTypeArray = (schema) => {
83
117
  return null;
84
118
  return schema.type;
85
119
  };
120
+ const stringBoundChecks = (valueExpr, schema) => {
121
+ const checks = [];
122
+ if (hasPattern(schema))
123
+ checks.push(`${regexLiteral(schema.pattern)}.test(${valueExpr})`);
124
+ if (hasMinLength(schema))
125
+ checks.push(minLengthPassExpr(valueExpr, schema.minLength));
126
+ if (hasMaxLength(schema))
127
+ checks.push(maxLengthPassExpr(valueExpr, schema.maxLength));
128
+ return checks;
129
+ };
130
+ const numberBoundChecks = (valueExpr, schema) => {
131
+ const checks = [];
132
+ if (hasMinimum(schema))
133
+ checks.push(`${valueExpr} >= ${schema.minimum}`);
134
+ if (hasMaximum(schema))
135
+ checks.push(`${valueExpr} <= ${schema.maximum}`);
136
+ if (hasExclusiveMinimum(schema))
137
+ checks.push(`${valueExpr} > ${schema.exclusiveMinimum}`);
138
+ if (hasExclusiveMaximum(schema))
139
+ checks.push(`${valueExpr} < ${schema.exclusiveMaximum}`);
140
+ if (hasMultipleOf(schema))
141
+ checks.push(multipleOfPassExpr(valueExpr, schema.multipleOf));
142
+ return checks;
143
+ };
86
144
  const getTypeCoercion = (accessor, schema, defaultValue) => {
87
145
  if (!hasType(schema)) {
88
146
  return null;
89
147
  }
90
148
  switch (schema.type) {
91
- case "string":
92
- return `String(${accessor})`;
93
- case "number": {
94
- const n = `Number(${accessor})`;
95
- return `(Number.isFinite(${n}) ? ${n} : ${defaultValue})`;
149
+ case "string": {
150
+ const coerced = `String(${accessor})`;
151
+ const bounds = stringBoundChecks("_s", schema);
152
+ if (bounds.length === 0)
153
+ return coerced;
154
+ return `((_s) => ${bounds.join(" && ")} ? _s : ${defaultValue})(${coerced})`;
96
155
  }
156
+ case "number":
97
157
  case "integer": {
158
+ const wellFormed = schema.type === "integer" ? "Number.isInteger" : "Number.isFinite";
159
+ const bounds = numberBoundChecks("_n", schema);
98
160
  const n = `Number(${accessor})`;
99
- return `(Number.isInteger(${n}) ? ${n} : ${defaultValue})`;
161
+ if (bounds.length === 0)
162
+ return `(${wellFormed}(${n}) ? ${n} : ${defaultValue})`;
163
+ return `((_n) => ${[`${wellFormed}(_n)`, ...bounds].join(" && ")} ? _n : ${defaultValue})(${n})`;
100
164
  }
101
165
  case "boolean":
102
166
  return `Boolean(${accessor})`;
103
167
  case "array":
104
- return `[]`;
168
+ return defaultValue;
105
169
  case "object":
106
170
  return `typeof ${accessor} === "object" && ${accessor} !== null ? ${accessor} : ${defaultValue}`;
107
171
  default:
@@ -166,9 +230,9 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
166
230
  for (let i = 1; i < allChecks.length; i++) {
167
231
  combinedCheck2 += " && " + allChecks[i];
168
232
  }
169
- return `${combinedCheck2} ? ${accessor} : ${defaultValue}`;
233
+ return `${combinedCheck2} ? ${accessor} : ${isRequired ? defaultValue : "undefined"}`;
170
234
  }
171
- return `${accessor} ?? ${defaultValue}`;
235
+ return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
172
236
  }
173
237
  if ("not" in schema && schema.not) {
174
238
  const notChecks = generateSchemaChecks(accessor, schema.not);
@@ -177,7 +241,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
177
241
  for (let i = 1; i < notChecks.length; i++) {
178
242
  combinedNotCheck += " && " + notChecks[i];
179
243
  }
180
- return `!(${combinedNotCheck}) ? ${accessor} : ${defaultValue}`;
244
+ return `!(${combinedNotCheck}) ? ${accessor} : ${isRequired ? defaultValue : "undefined"}`;
181
245
  }
182
246
  }
183
247
  if (hasConst(schema)) {
@@ -193,7 +257,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
193
257
  }
194
258
  }
195
259
  if (!hasType(schema) && !hasEnum(schema)) {
196
- return `${accessor} ?? ${defaultValue}`;
260
+ return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
197
261
  }
198
262
  const prefixItems = getPrefixItems(schema);
199
263
  if (hasType(schema) && schema.type === "array" && prefixItems) {
@@ -215,10 +279,10 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
215
279
  checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
216
280
  }
217
281
  if (hasMinLength(schema)) {
218
- checks.push(`${accessor}.length >= ${schema.minLength}`);
282
+ checks.push(minLengthPassExpr(accessor, schema.minLength));
219
283
  }
220
284
  if (hasMaxLength(schema)) {
221
- checks.push(`${accessor}.length <= ${schema.maxLength}`);
285
+ checks.push(maxLengthPassExpr(accessor, schema.maxLength));
222
286
  }
223
287
  break;
224
288
  }
@@ -336,6 +400,7 @@ export {
336
400
  generateValidationExpression,
337
401
  getPrefixItems,
338
402
  isCoercibleItemSchema,
403
+ plainScalarItemCheck,
339
404
  prefixItemsCapsLength,
340
405
  scalarItemTypeCheck,
341
406
  singleTypeCheck
@@ -73,7 +73,7 @@ const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
73
73
  const depthOf = (ctx) => ctx.depth ?? 0;
74
74
  const nest = (ctx) => ({ ...ctx, depth: depthOf(ctx) + 1 });
75
75
  const isObjectExpr = (acc) => `typeof ${acc} === "object" && ${acc} !== null && !Array.isArray(${acc})`;
76
- const recordOf = (acc) => `(${acc} as Record<string, unknown>)`;
76
+ const recordOf = (acc) => `(${acc} as Record<string, any>)`;
77
77
  const patternEntriesOf = (schema) => {
78
78
  const source = schema["patternProperties"];
79
79
  if (typeof source !== "object" || source === null || Array.isArray(source))
@@ -118,7 +118,7 @@ const tupleShape = (schema) => {
118
118
  const arrayConstraints = (acc, schema, ctx) => {
119
119
  const c = [];
120
120
  const d = depthOf(ctx);
121
- const elements = `(${acc} as unknown[])`;
121
+ const elements = `(${acc} as any[])`;
122
122
  if (hasMinItems(schema))
123
123
  c.push(`${acc}.length >= ${schema.minItems}`);
124
124
  if (hasMaxItems(schema))
@@ -182,7 +182,7 @@ const containsConstraint = (acc, schema, ctx) => {
182
182
  const match = subschemaMatchExpr(el, s["contains"], nest(ctx));
183
183
  if (match === null)
184
184
  return null;
185
- const count = `(${acc} as unknown[]).filter((${el}) => ${match}).length`;
185
+ const count = `(${acc} as any[]).filter((${el}) => ${match}).length`;
186
186
  const bounds = [];
187
187
  if (min > 0)
188
188
  bounds.push(`${count} >= ${min}`);
@@ -52,7 +52,7 @@ const ownItemCoverage = (schema, indexVar, ignoreOwnUnevaluated) => {
52
52
  const containsCoverage = (schema, indexVar, acc, ctx, match) => {
53
53
  if (!("contains" in schema))
54
54
  return NONE;
55
- const itemMatch = match(`(${acc} as unknown[])[${indexVar}]`, schema["contains"], nest(ctx));
55
+ const itemMatch = match(`(${acc} as any[])[${indexVar}]`, schema["contains"], nest(ctx));
56
56
  if (itemMatch === null)
57
57
  return null;
58
58
  if (itemMatch === "true")
@@ -186,7 +186,7 @@ const unevaluatedItemsExpr = (acc, schema, ctx, match) => {
186
186
  return null;
187
187
  if (coverage.all)
188
188
  return void 0;
189
- const elements = `(${acc} as unknown[])`;
189
+ const elements = `(${acc} as any[])`;
190
190
  const covered = coverage.terms.length > 0 ? orJoin(coverage.terms) : null;
191
191
  if (unevaluated === false) {
192
192
  if (covered === null)
@@ -44,6 +44,40 @@ const arrayFallback = (schema) => {
44
44
  }
45
45
  return `[${elements.join(", ")}]`;
46
46
  };
47
+ const MAX_FALLBACK_LENGTH = 256;
48
+ const stringFallback = (schema) => {
49
+ if (hasPattern(schema)) {
50
+ const patternDefault = generateDefaultFromPattern(schema.pattern);
51
+ if (patternDefault)
52
+ return patternDefault;
53
+ return '""';
54
+ }
55
+ const minLength = schema["minLength"];
56
+ if (typeof minLength !== "number" || !(minLength > 0))
57
+ return '""';
58
+ return JSON.stringify("x".repeat(Math.min(minLength, MAX_FALLBACK_LENGTH)));
59
+ };
60
+ const finiteKeyword = (schema, keyword) => {
61
+ const value = schema[keyword];
62
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
63
+ };
64
+ const numberFallback = (schema, integer) => {
65
+ const minimum = finiteKeyword(schema, "minimum");
66
+ const maximum = finiteKeyword(schema, "maximum");
67
+ const exclusiveMinimum = finiteKeyword(schema, "exclusiveMinimum");
68
+ const exclusiveMaximum = finiteKeyword(schema, "exclusiveMaximum");
69
+ const multipleOf = finiteKeyword(schema, "multipleOf");
70
+ const low = Math.max(minimum ?? -Infinity, exclusiveMinimum !== void 0 ? exclusiveMinimum + 1 : -Infinity);
71
+ const high = Math.min(maximum ?? Infinity, exclusiveMaximum !== void 0 ? exclusiveMaximum - 1 : Infinity);
72
+ let candidate = Math.min(Math.max(0, low), high);
73
+ if (integer)
74
+ candidate = Math.ceil(candidate);
75
+ if (multipleOf !== void 0 && multipleOf > 0 && candidate !== 0) {
76
+ candidate = Math.ceil(candidate / multipleOf) * multipleOf;
77
+ }
78
+ const satisfies = Number.isFinite(candidate) && (minimum === void 0 || candidate >= minimum) && (maximum === void 0 || candidate <= maximum) && (exclusiveMinimum === void 0 || candidate > exclusiveMinimum) && (exclusiveMaximum === void 0 || candidate < exclusiveMaximum) && (!integer || Number.isInteger(candidate)) && (multipleOf === void 0 || multipleOf <= 0 || Number.isInteger(candidate / multipleOf));
79
+ return satisfies ? String(candidate) : "0";
80
+ };
47
81
  const getDefaultValue = (schema) => {
48
82
  if (!isSchemaObject(schema)) {
49
83
  return "undefined";
@@ -89,18 +123,11 @@ const getDefaultValue = (schema) => {
89
123
  return "undefined";
90
124
  }
91
125
  switch (schema.type) {
92
- case "string": {
93
- if (hasPattern(schema)) {
94
- const patternDefault = generateDefaultFromPattern(schema.pattern);
95
- if (patternDefault) {
96
- return patternDefault;
97
- }
98
- }
99
- return '""';
100
- }
126
+ case "string":
127
+ return stringFallback(schema);
101
128
  case "number":
102
129
  case "integer":
103
- return "0";
130
+ return numberFallback(schema, schema.type === "integer");
104
131
  case "boolean":
105
132
  return "false";
106
133
  case "null":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.19.4",
3
+ "version": "0.20.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -56,10 +56,10 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "json-schema-typed": "^8.0.1",
59
- "@amritk/helpers": "^0.15.4"
59
+ "@amritk/helpers": "^0.17.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@amritk/generate-markdown": "^0.4.9",
62
+ "@amritk/generate-markdown": "^0.5.0",
63
63
  "@sinclair/typebox": "^0.34.49",
64
64
  "ajv": "^8.17.1",
65
65
  "zod": "^4.4.3"