@amritk/generate-parsers 0.19.3 → 0.19.5

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,23 @@ 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
313
 
314
314
  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
315
+ parser source — which measures **~0.2–0.8 ms** per schema here (zod and TypeBox
316
316
  author or interpret their parsers with no separate build step, so there is
317
317
  nothing to time). That is trivially amortized: you generate once at build time
318
318
  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,7 +548,7 @@ 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
  };
558
554
  const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES, caseInsensitive = false, shapeValidatorSource) => {
@@ -592,9 +588,11 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
592
588
  }
593
589
  const strictKeys = hasStrictKeys(schema);
594
590
  const stripKeys = strictKeys || stripUnknown;
595
- const exactKeyCount = stripKeys ? exactKeyCountOf(schema) : null;
591
+ const rejectsUnknownKeys = strict && isSchemaObject(schema) && hasAdditionalProperties(schema) && schema.additionalProperties === false && !("patternProperties" in schema) || false;
592
+ const guardKeys = stripKeys || rejectsUnknownKeys;
593
+ const exactKeyCount = guardKeys ? exactKeyCountOf(schema) : null;
596
594
  const strictKeyCheck = unknownKeyCheck(propertyKeys, `_knownKeys${typeName}`);
597
- if (stripKeys && exactKeyCount === null) {
595
+ if (guardKeys && exactKeyCount === null) {
598
596
  for (const declaration of strictKeyCheck.declarations) {
599
597
  preamble.push(`${declaration};`);
600
598
  }
@@ -602,7 +600,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
602
600
  for (const _k in input) if (${strictKeyCheck.isUnknown("_k")}) return false;
603
601
  return true;
604
602
  };`);
605
- } else if (strict && strictKeys) {
603
+ } else if (rejectsUnknownKeys) {
606
604
  for (const declaration of strictKeyCheck.declarations) {
607
605
  preamble.push(`${declaration};`);
608
606
  }
@@ -661,7 +659,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
661
659
  fastPathAccessorChecks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
662
660
  }
663
661
  }
664
- if (stripKeys) {
662
+ if (guardKeys) {
665
663
  fastPathChecks.push(exactKeyCount !== null ? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}` : `_hasOnlyKnownKeys${typeName}(input)`);
666
664
  }
667
665
  const deepGuard = canFastPath && fastPathChecks.length > 0 ? fastPathChecks.join(" && ") : null;
@@ -710,7 +708,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
710
708
  const shallowGuard = canShallowGuard && shallowChecks.length > 0 ? shallowChecks.join(" && ") : null;
711
709
  const varDeclLines = [];
712
710
  for (const { key, varName, propSchema } of propInfo) {
713
- if (shouldCacheVariable(propSchema, canFastPath, useRefImports)) {
711
+ if (shouldCacheVariable(propSchema)) {
714
712
  varDeclLines.push(` const ${varName} = ${safeAccessor("input", key)};`);
715
713
  }
716
714
  }
@@ -740,7 +738,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
740
738
  }
741
739
  }
742
740
  for (const { key, varName, isRequired, propSchema } of propInfo) {
743
- const shouldCache = shouldCacheVariable(propSchema, canFastPath, useRefImports);
741
+ const shouldCache = shouldCacheVariable(propSchema);
744
742
  const accessor = shouldCache ? varName : safeAccessor("input", key);
745
743
  const subName = subTypeNames.get(key);
746
744
  if (subName) {
@@ -848,12 +846,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
848
846
  }
849
847
  continue;
850
848
  }
851
- const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports) ? varName : safeAccessor("input", key);
849
+ const accessor = shouldCacheVariable(propSchema) ? varName : safeAccessor("input", key);
852
850
  fields.push(isRequired ? ` ${safeLiteralKey(key)}: ${accessor},` : ` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
853
851
  }
854
852
  lines2.push(` if (${deepGuardExpr}) return {`);
855
853
  lines2.push(fields.join("\n"));
856
- lines2.push(` } as ${typeName};`);
854
+ lines2.push(` } as unknown as ${typeName};`);
857
855
  };
858
856
  const lines = [];
859
857
  lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
@@ -903,7 +901,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
903
901
  for (const assertionLine of assertionLines) {
904
902
  lines.push(assertionLine);
905
903
  }
906
- if (strictKeys) {
904
+ if (rejectsUnknownKeys) {
907
905
  lines.push(` for (const _k in input) {`);
908
906
  lines.push(` if (${strictKeyCheck.isUnknown("_k")}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
909
907
  lines.push(` }`);
@@ -1280,6 +1278,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1280
1278
  stripUnknown,
1281
1279
  ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {}
1282
1280
  };
1281
+ const copyLoopContext = { ...context, subParsers: false };
1283
1282
  if (!options?.isRoot && typeName === `Schema${suffix}`) {
1284
1283
  return generateSchemaObjectParser(typeName);
1285
1284
  }
@@ -1296,7 +1295,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1296
1295
  });
1297
1296
  }
1298
1297
  if (hasProperties(schema) && isSchemaObject(schema) && "patternProperties" in schema) {
1299
- return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive, context);
1298
+ return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive, copyLoopContext);
1300
1299
  }
1301
1300
  if (strict && isSchemaObject(schema) && Array.isArray(schema.type) && schema.type.includes("null")) {
1302
1301
  return generateNonObjectParser(typeName, schema, strict, {
@@ -1342,14 +1341,14 @@ const selectParserStrategy = (schema, typeName, options) => {
1342
1341
  });
1343
1342
  }
1344
1343
  if ("patternProperties" in schema) {
1345
- return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict, context);
1344
+ return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict, copyLoopContext);
1346
1345
  }
1347
1346
  if ("additionalProperties" in schema && !hasProperties(schema)) {
1348
1347
  const additionalProps = schema.additionalProperties;
1349
1348
  if (additionalProps === true || additionalProps === false) {
1350
1349
  return generateEmptyObjectParser(typeName, strict, schema, context);
1351
1350
  }
1352
- return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict, context);
1351
+ return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict, copyLoopContext);
1353
1352
  }
1354
1353
  if ("type" in schema && schema.type === "object" && !hasProperties(schema)) {
1355
1354
  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,12 +4,14 @@ 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";
10
11
  import { generateEnumCaseInsensitiveCoercion, generateEnumCheck } from "./generate-enum-check.js";
11
12
  import { generateNonDiscriminatedUnionValidation } from "./generate-non-discriminated-union-validation.js";
12
13
  import { generateSchemaChecks } from "./generate-schema-checks.js";
14
+ import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
13
15
  const getInstanceCoercion = (accessor, instanceOf, defaultValue) => {
14
16
  if (instanceOf === "Date")
15
17
  return `((_d) => Number.isNaN(_d.getTime()) ? ${defaultValue} : _d)(new Date(${accessor} as string | number | Date))`;
@@ -33,10 +35,43 @@ const scalarItemTypeCheck = (itemSchema, accessor) => {
33
35
  return null;
34
36
  }
35
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
+ };
36
63
  const isCoercibleItemSchema = (itemSchema) => {
37
64
  if (scalarItemTypeCheck(itemSchema, "_it") !== null)
38
65
  return true;
39
- 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;
40
75
  };
41
76
  const getPrefixItems = (schema) => {
42
77
  if (!isSchemaObject(schema))
@@ -49,13 +84,13 @@ const getPrefixItems = (schema) => {
49
84
  const everyTailItem = (accessor, itemCheck, schema) => {
50
85
  const prefix = isSchemaObject(schema) ? schema["prefixItems"] : void 0;
51
86
  const tailStart = Array.isArray(prefix) ? prefix.length : 0;
52
- 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}))`;
53
88
  };
54
89
  const prefixItemsCapsLength = (schema) => {
55
90
  if (!isSchemaObject(schema))
56
91
  return false;
57
92
  const s = schema;
58
- return s["items"] === false || s["additionalItems"] === false;
93
+ return (Object.hasOwn(s, "items") ? s["items"] : void 0) === false || (Object.hasOwn(s, "additionalItems") ? s["additionalItems"] : void 0) === false;
59
94
  };
60
95
  const singleTypeCheck = (accessor, type) => {
61
96
  switch (type) {
@@ -82,25 +117,55 @@ const getTypeArray = (schema) => {
82
117
  return null;
83
118
  return schema.type;
84
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
+ };
85
144
  const getTypeCoercion = (accessor, schema, defaultValue) => {
86
145
  if (!hasType(schema)) {
87
146
  return null;
88
147
  }
89
148
  switch (schema.type) {
90
- case "string":
91
- return `String(${accessor})`;
92
- case "number": {
93
- const n = `Number(${accessor})`;
94
- 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})`;
95
155
  }
156
+ case "number":
96
157
  case "integer": {
158
+ const wellFormed = schema.type === "integer" ? "Number.isInteger" : "Number.isFinite";
159
+ const bounds = numberBoundChecks("_n", schema);
97
160
  const n = `Number(${accessor})`;
98
- 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})`;
99
164
  }
100
165
  case "boolean":
101
166
  return `Boolean(${accessor})`;
102
167
  case "array":
103
- return `[]`;
168
+ return defaultValue;
104
169
  case "object":
105
170
  return `typeof ${accessor} === "object" && ${accessor} !== null ? ${accessor} : ${defaultValue}`;
106
171
  default:
@@ -165,9 +230,9 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
165
230
  for (let i = 1; i < allChecks.length; i++) {
166
231
  combinedCheck2 += " && " + allChecks[i];
167
232
  }
168
- return `${combinedCheck2} ? ${accessor} : ${defaultValue}`;
233
+ return `${combinedCheck2} ? ${accessor} : ${isRequired ? defaultValue : "undefined"}`;
169
234
  }
170
- return `${accessor} ?? ${defaultValue}`;
235
+ return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
171
236
  }
172
237
  if ("not" in schema && schema.not) {
173
238
  const notChecks = generateSchemaChecks(accessor, schema.not);
@@ -176,7 +241,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
176
241
  for (let i = 1; i < notChecks.length; i++) {
177
242
  combinedNotCheck += " && " + notChecks[i];
178
243
  }
179
- return `!(${combinedNotCheck}) ? ${accessor} : ${defaultValue}`;
244
+ return `!(${combinedNotCheck}) ? ${accessor} : ${isRequired ? defaultValue : "undefined"}`;
180
245
  }
181
246
  }
182
247
  if (hasConst(schema)) {
@@ -192,7 +257,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
192
257
  }
193
258
  }
194
259
  if (!hasType(schema) && !hasEnum(schema)) {
195
- return `${accessor} ?? ${defaultValue}`;
260
+ return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
196
261
  }
197
262
  const prefixItems = getPrefixItems(schema);
198
263
  if (hasType(schema) && schema.type === "array" && prefixItems) {
@@ -214,10 +279,10 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
214
279
  checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
215
280
  }
216
281
  if (hasMinLength(schema)) {
217
- checks.push(`${accessor}.length >= ${schema.minLength}`);
282
+ checks.push(minLengthPassExpr(accessor, schema.minLength));
218
283
  }
219
284
  if (hasMaxLength(schema)) {
220
- checks.push(`${accessor}.length <= ${schema.maxLength}`);
285
+ checks.push(maxLengthPassExpr(accessor, schema.maxLength));
221
286
  }
222
287
  break;
223
288
  }
@@ -259,7 +324,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
259
324
  checks.push(`${accessor}.length <= ${schema.maxItems}`);
260
325
  }
261
326
  if (hasUniqueItems(schema) && schema.uniqueItems === true) {
262
- checks.push(`new Set(${accessor}).size === ${accessor}.length`);
327
+ checks.push(generateUniqueItemsCheck(accessor, schema));
263
328
  }
264
329
  break;
265
330
  }
@@ -335,6 +400,7 @@ export {
335
400
  generateValidationExpression,
336
401
  getPrefixItems,
337
402
  isCoercibleItemSchema,
403
+ plainScalarItemCheck,
338
404
  prefixItemsCapsLength,
339
405
  scalarItemTypeCheck,
340
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)
@@ -7,17 +7,17 @@ const collectImports = (schema, options) => {
7
7
  const typesOnly = options?.typesOnly === true;
8
8
  const importExt = options?.importExt ?? "js";
9
9
  const importMap = /* @__PURE__ */ new Map();
10
- for (const [filename, typeName] of collectImportTargets(schema, options)) {
10
+ for (const [filename, { typeName, typeOnly }] of collectImportTargets(schema, options)) {
11
11
  const importPath = getImportPathForFilename(filename, importExt);
12
- const importStatement = typesOnly ? `import type { ${typeName} } from '${importPath}';` : `import { type ${typeName}, parse${typeName}, validate${typeName}Shape } from '${importPath}';`;
12
+ const importStatement = typesOnly || typeOnly ? `import type { ${typeName} } from '${importPath}';` : `import { type ${typeName}, parse${typeName}, validate${typeName}Shape } from '${importPath}';`;
13
13
  importMap.set(filename, importStatement);
14
14
  }
15
15
  return Array.from(importMap.values()).sort();
16
16
  };
17
17
  const collectImportTypeNames = (schema, options) => {
18
18
  const names = /* @__PURE__ */ new Set();
19
- for (const [, typeName] of collectImportTargets(schema, options))
20
- names.add(typeName);
19
+ for (const [, target] of collectImportTargets(schema, options))
20
+ names.add(target.typeName);
21
21
  return names;
22
22
  };
23
23
  const collectImportTargets = (schema, options) => {
@@ -25,6 +25,8 @@ const collectImportTargets = (schema, options) => {
25
25
  const rootSchema = options?.rootSchema;
26
26
  const typeSuffix = options?.typeSuffix;
27
27
  const refs = /* @__PURE__ */ new Set();
28
+ const valueRefs = /* @__PURE__ */ new Set();
29
+ let typeOnlyDepth = 0;
28
30
  const collectRefsFromValue = (value) => {
29
31
  if (typeof value !== "object" || value === null) {
30
32
  return;
@@ -43,21 +45,34 @@ const collectImportTargets = (schema, options) => {
43
45
  const isPropertyFragment = isUri && ref.includes("#/properties/");
44
46
  if (isInternal || isUri && !isPropertyFragment) {
45
47
  refs.add(ref);
48
+ if (typeOnlyDepth === 0)
49
+ valueRefs.add(ref);
46
50
  }
47
51
  return;
48
52
  }
53
+ for (const keyword of ["prefixItems", "items"]) {
54
+ const positions = Object.hasOwn(record, keyword) ? record[keyword] : void 0;
55
+ if (!Array.isArray(positions))
56
+ continue;
57
+ typeOnlyDepth++;
58
+ for (const item of positions) {
59
+ collectRefsFromValue(item);
60
+ }
61
+ if (Object.hasOwn(record, "additionalItems"))
62
+ collectRefsFromValue(record["additionalItems"]);
63
+ typeOnlyDepth--;
64
+ }
49
65
  if (hasAdditionalProperties(record) && hasRef(record.additionalProperties)) {
50
66
  collectRefsFromValue(record.additionalProperties);
51
67
  return;
52
68
  }
53
- if (record.type === "array" && hasItems(record) && hasRef(record.items)) {
69
+ if (record.type === "array" && hasItems(record) && !Array.isArray(record.items) && hasRef(record.items)) {
54
70
  collectRefsFromValue(record.items);
55
71
  return;
56
72
  }
57
- if ("properties" in record && typeof record.properties === "object" && record.properties !== null) {
58
- const props = record.properties;
59
- for (const key in props) {
60
- collectRefsFromValue(props[key]);
73
+ if (Object.hasOwn(record, "properties") && typeof record.properties === "object" && record.properties !== null) {
74
+ for (const value2 of Object.values(record.properties)) {
75
+ collectRefsFromValue(value2);
61
76
  }
62
77
  }
63
78
  if (hasOneOf(record)) {
@@ -75,22 +90,24 @@ const collectImportTargets = (schema, options) => {
75
90
  collectRefsFromValue(item);
76
91
  }
77
92
  }
78
- if (hasItems(record)) {
93
+ if (hasItems(record) && !Array.isArray(record.items)) {
79
94
  collectRefsFromValue(record.items);
80
95
  }
81
96
  if (hasAdditionalProperties(record)) {
82
97
  collectRefsFromValue(record.additionalProperties);
83
98
  }
84
- if ("patternProperties" in record && typeof record.patternProperties === "object" && record.patternProperties !== null) {
85
- const patternProps = record.patternProperties;
86
- for (const key in patternProps) {
87
- collectRefsFromValue(patternProps[key]);
99
+ if (Object.hasOwn(record, "patternProperties") && typeof record.patternProperties === "object" && record.patternProperties !== null) {
100
+ for (const value2 of Object.values(record.patternProperties)) {
101
+ collectRefsFromValue(value2);
88
102
  }
89
103
  }
90
- if ("then" in record) {
104
+ if (Object.hasOwn(record, "if")) {
105
+ collectRefsFromValue(record.if);
106
+ }
107
+ if (Object.hasOwn(record, "then")) {
91
108
  collectRefsFromValue(record.then);
92
109
  }
93
- if ("else" in record) {
110
+ if (Object.hasOwn(record, "else")) {
94
111
  collectRefsFromValue(record.else);
95
112
  }
96
113
  };
@@ -101,26 +118,39 @@ const collectImportTargets = (schema, options) => {
101
118
  const isPropertyFragment = isUri && ref.includes("#/properties/");
102
119
  if (isInternal || isUri && !isPropertyFragment) {
103
120
  refs.add(ref);
121
+ valueRefs.add(ref);
104
122
  }
105
123
  }
106
- if (typeof schema === "object" && schema !== null && "properties" in schema) {
107
- const properties = schema.properties;
108
- for (const key in properties) {
109
- collectRefsFromValue(properties[key]);
124
+ if (typeof schema === "object" && schema !== null && Object.hasOwn(schema, "properties") && typeof schema.properties === "object" && schema.properties !== null) {
125
+ for (const value of Object.values(schema.properties)) {
126
+ collectRefsFromValue(value);
110
127
  }
111
128
  }
112
- if (typeof schema === "object" && schema !== null && "additionalProperties" in schema) {
129
+ if (typeof schema === "object" && schema !== null && Object.hasOwn(schema, "additionalProperties")) {
113
130
  collectRefsFromValue(schema.additionalProperties);
114
131
  }
115
- if (typeof schema === "object" && schema !== null && "patternProperties" in schema) {
116
- const patternProps = schema.patternProperties;
117
- for (const key in patternProps) {
118
- collectRefsFromValue(patternProps[key]);
132
+ if (typeof schema === "object" && schema !== null && Object.hasOwn(schema, "patternProperties") && typeof schema.patternProperties === "object" && schema.patternProperties !== null) {
133
+ for (const value of Object.values(schema.patternProperties)) {
134
+ collectRefsFromValue(value);
119
135
  }
120
136
  }
121
- if (typeof schema === "object" && schema !== null && hasItems(schema)) {
137
+ if (typeof schema === "object" && schema !== null && hasItems(schema) && !Array.isArray(schema.items)) {
122
138
  collectRefsFromValue(schema.items);
123
139
  }
140
+ if (typeof schema === "object" && schema !== null) {
141
+ const record = schema;
142
+ for (const keyword of ["prefixItems", "items"]) {
143
+ const positions = Object.hasOwn(record, keyword) ? record[keyword] : void 0;
144
+ if (!Array.isArray(positions))
145
+ continue;
146
+ typeOnlyDepth++;
147
+ for (const item of positions)
148
+ collectRefsFromValue(item);
149
+ if (Object.hasOwn(record, "additionalItems"))
150
+ collectRefsFromValue(record["additionalItems"]);
151
+ typeOnlyDepth--;
152
+ }
153
+ }
124
154
  if (typeof schema === "object" && schema !== null && hasOneOf(schema)) {
125
155
  for (const item of schema.oneOf) {
126
156
  collectRefsFromValue(item);
@@ -136,13 +166,13 @@ const collectImportTargets = (schema, options) => {
136
166
  collectRefsFromValue(item);
137
167
  }
138
168
  }
139
- if (typeof schema === "object" && schema !== null && "if" in schema) {
169
+ if (typeof schema === "object" && schema !== null && Object.hasOwn(schema, "if")) {
140
170
  collectRefsFromValue(schema.if);
141
171
  }
142
- if (typeof schema === "object" && schema !== null && "then" in schema) {
172
+ if (typeof schema === "object" && schema !== null && Object.hasOwn(schema, "then")) {
143
173
  collectRefsFromValue(schema.then);
144
174
  }
145
- if (typeof schema === "object" && schema !== null && "else" in schema) {
175
+ if (typeof schema === "object" && schema !== null && Object.hasOwn(schema, "else")) {
146
176
  collectRefsFromValue(schema.else);
147
177
  }
148
178
  const targets = /* @__PURE__ */ new Map();
@@ -156,7 +186,8 @@ const collectImportTargets = (schema, options) => {
156
186
  if (selfFilename !== void 0 && filename === selfFilename) {
157
187
  continue;
158
188
  }
159
- targets.set(filename, typeName);
189
+ const typeOnly = !valueRefs.has(ref) && (targets.get(filename)?.typeOnly ?? true);
190
+ targets.set(filename, { typeName, typeOnly });
160
191
  }
161
192
  return targets;
162
193
  };
@@ -26,8 +26,8 @@ const defaultMatchesType = (value, schema) => {
26
26
  const MAX_FALLBACK_ITEMS = 32;
27
27
  const arrayFallback = (schema) => {
28
28
  const record = schema;
29
- const positions = Array.isArray(record["prefixItems"]) ? record["prefixItems"] : Array.isArray(record["items"]) ? record["items"] : [];
30
- const tail = Array.isArray(record["items"]) ? record["additionalItems"] : record["items"];
29
+ const positions = Array.isArray(Object.hasOwn(record, "prefixItems") ? record["prefixItems"] : void 0) ? Object.hasOwn(record, "prefixItems") ? record["prefixItems"] : void 0 : Array.isArray(Object.hasOwn(record, "items") ? record["items"] : void 0) ? Object.hasOwn(record, "items") ? record["items"] : void 0 : [];
30
+ const tail = Array.isArray(Object.hasOwn(record, "items") ? record["items"] : void 0) ? Object.hasOwn(record, "additionalItems") ? record["additionalItems"] : void 0 : Object.hasOwn(record, "items") ? record["items"] : void 0;
31
31
  const minItems = typeof record["minItems"] === "number" ? record["minItems"] : 0;
32
32
  const wanted = minItems > MAX_FALLBACK_ITEMS ? Math.min(minItems, positions.length) : minItems;
33
33
  if (wanted === 0)
@@ -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.3",
3
+ "version": "0.19.5",
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.3"
59
+ "@amritk/helpers": "^0.16.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@amritk/generate-markdown": "^0.4.8",
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"