@amritk/generate-parsers 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  **Programmatic API for generating TypeScript parsers and type definitions from JSON Schemas.**
6
6
 
7
7
  ![status](https://img.shields.io/badge/status-pre--alpha-ef4444?style=flat-square) 
8
- ![version](https://img.shields.io/badge/version-v0.1.0-6366f1?style=flat-square&logo=npm&logoColor=white) 
8
+ ![version](https://img.shields.io/npm/v/@amritk/generate-parsers?style=flat-square&logo=npm&logoColor=white&label=version&color=6366f1) 
9
9
  ![license](https://img.shields.io/badge/license-MIT-22c55e?style=flat-square) 
10
10
  ![JSON Schema](https://img.shields.io/badge/JSON%20Schema-2020--12-f97316?style=flat-square) 
11
11
  ![node](https://img.shields.io/badge/node-%E2%89%A520-339933?style=flat-square&logo=node.js&logoColor=white) 
@@ -75,19 +75,33 @@ Write them to disk however you like.
75
75
 
76
76
  ## API
77
77
 
78
- ### `buildSchema(rootSchema, rootTypeName, extensions?, typesOnly?, logWarnings?, strict?)`
79
-
80
- | Parameter | Type | Description |
81
- |:---|:---|:---|
82
- | `rootSchema` | `JSONSchema` | The root schema to traverse. `$ref` and `$dynamicRef` are resolved recursively. |
83
- | `rootTypeName` | `string` | Name used for the root type (e.g. `"Document"`). |
84
- | `extensions` | `SchemaExtensions` _(optional)_ | Map of definition name → extra optional properties to merge in before generation. |
85
- | `typesOnly` | `boolean` _(optional)_ | When `true`, only emit `.ts` type definitions — skip parser functions and runtime helpers. |
86
- | `logWarnings` | `boolean` _(optional)_ | When `true`, generated parsers emit a `console.warn` for every input key not declared in the schema's properties. |
87
- | `strict` | `boolean` _(optional)_ | When `true`, generated parsers throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. |
78
+ ### `buildSchema(rootSchema, rootTypeName, …options)`
79
+
80
+ Every option after the first two is **positional** — pass them in this order:
81
+
82
+ | # | Parameter | Type | Default | Description |
83
+ |--:|:---|:---|:---|:---|
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"`). |
86
+ | 3 | `extensions` | `SchemaExtensions` | — | Map of definition name → extra optional properties to merge in before generation. |
87
+ | 4 | `typesOnly` | `boolean` | — | Only emit `.ts` type definitions — skip parser functions and runtime helpers. |
88
+ | 5 | `logWarnings` | `boolean` | — | Generated parsers emit a `console.warn` for every input key not declared in the schema's properties. |
89
+ | 6 | `strict` | `boolean` | — | Generated parsers throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. |
90
+ | 7 | `helpersMode` | `'package' \| 'embedded'` | `'package'` | `'package'` imports runtime helpers from `@amritk/helpers`; `'embedded'` emits them alongside the output so it is self-contained. |
91
+ | 8 | `helpersImportPrefix` | `string` | `'./'` | Prefix the generated files use when importing embedded helpers. |
92
+ | 9 | `readonly` | `boolean` | `false` | Emit every property, array, and record in the type definitions as `readonly`. |
93
+ | 10 | `stripUnknown` | `boolean` | `false` | Build each result from the declared properties only, dropping undeclared input keys at every level (zod's `.strip()`). |
94
+ | 11 | `typeSuffix` | `string` | `''` | Suffix appended to every `$ref`-derived type name (`'Object'` turns `Contact` into `ContactObject`). The root type name is unaffected. |
95
+ | 12 | `importExt` | `'js' \| 'ts'` | `'js'` | Extension emitted on relative import specifiers. `'js'` is the NodeNext form for output you compile; `'ts'` emits the literal on-disk paths so the sources run unbuilt. |
96
+ | 13 | `caseInsensitive` | `boolean` | `false` | Normalize a mis-cased string to the exact casing of an enum/const member it matches case-insensitively. Coerce mode only. |
88
97
 
89
98
  Returns: `Promise<GeneratedFile[]>`.
90
99
 
100
+ > [!NOTE]
101
+ > `importExt` defaults to `'js'` here, whereas the [`mjst` CLI](../cli) defaults it to
102
+ > `'ts'`. The example above therefore emits `./info.js` specifiers; pass `'ts'` for
103
+ > output meant to run without a build step.
104
+
91
105
  ---
92
106
 
93
107
  ## Options
@@ -151,7 +165,9 @@ The generator handles:
151
165
  - `$ref` and `$dynamicRef` resolution, including JSON Schema 2020-12 `$dynamicAnchor`
152
166
  - Discriminated and non-discriminated unions (`oneOf` / `anyOf`)
153
167
  - Enums and `const` values
154
- - Nested objects, arrays, records, and tuples
168
+ - Nested objects, arrays, records, and tuples (`prefixItems`, and the draft-07 array form of `items`)
169
+ - Multi-type schemas (`type: ["string", "null"]`) and the OpenAPI 3.0 `nullable: true` spelling
170
+ - Composition alongside a declared shape — `allOf` members and sibling unions are intersected into the type, not dropped
155
171
  - Pattern-based default values
156
172
 
157
173
  ---
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, resolve as resolvePath } from "node:path";
4
+ import { foldNullable } from "@amritk/helpers/fold-nullable";
4
5
  import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel";
5
6
  import { walkRefGraph } from "@amritk/helpers/walk-ref-graph";
6
7
  import { applySchemaExtensions } from "../helpers/apply-schema-extensions.js";
@@ -22,11 +23,13 @@ const readHelperSource = async (helper, importExt) => {
22
23
  const buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false, stripUnknown = false, typeSuffix = "", importExt = "js", caseInsensitive = false) => {
23
24
  const files = [];
24
25
  const usedHelpers = /* @__PURE__ */ new Set();
25
- walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
26
+ const foldedSchema = foldNullable(rootSchema);
27
+ walkRefGraph(foldedSchema, rootTypeName, { typeSuffix }, (node) => {
26
28
  if (node.filename === "index")
27
29
  return;
28
30
  const extended = extensions ? applySchemaExtensions(node.schema, node.filename, extensions) : node.schema;
29
31
  const result = generateFile(extended, node.typeName, {
32
+ selfFilename: node.filename,
30
33
  typesOnly: typesOnly ?? false,
31
34
  rootSchema: node.rootSchema,
32
35
  helpersMode,
@@ -1,8 +1,7 @@
1
- import { safeAccessor } from "@amritk/helpers/safe-accessor";
2
1
  import { getDiscriminatorValue } from "../helpers/get-discriminator-value.js";
3
2
  import { generateSchemaChecks } from "./generate-schema-checks.js";
4
3
  const generateDiscriminatedUnionValidation = (accessor, schemas, discriminatorKey, defaultValue, isRequired) => {
5
- const discAccessor = safeAccessor(`${accessor}?`, discriminatorKey);
4
+ const discAccessor = `(${accessor} as Record<string, unknown> | null | undefined)?.[${JSON.stringify(discriminatorKey)}]`;
6
5
  const branches = [];
7
6
  for (const schema of schemas) {
8
7
  const discriminatorValue = getDiscriminatorValue(schema, discriminatorKey);
@@ -16,6 +16,13 @@ type GenerateFileOptions = {
16
16
  * the import list, preventing a file from importing itself.
17
17
  */
18
18
  readonly selfRef?: string;
19
+ /**
20
+ * The filename this file is written as, without extension. Used for
21
+ * self-import detection when the schema has no `$ref` of its own — the root
22
+ * document — so a `$defs` entry that collides with the root type name cannot
23
+ * make the root file import itself.
24
+ */
25
+ readonly selfFilename?: string;
19
26
  /**
20
27
  * The root schema document. When provided, URI refs that cannot be resolved
21
28
  * within the root schema's $defs are excluded from the import list.
@@ -9,9 +9,20 @@ const generateFile = (schema, typeName, options) => {
9
9
  const helpersMode = options?.helpersMode ?? "package";
10
10
  const typeSuffix = options?.typeSuffix ?? "";
11
11
  const importExt = options?.importExt ?? "js";
12
- const typeDefinition = generateTypeDefinition(schema, typeName, { readonly: options?.readonly ?? false, typeSuffix });
12
+ const typeDefinition = generateTypeDefinition(schema, typeName, {
13
+ readonly: options?.readonly ?? false,
14
+ typeSuffix,
15
+ ...rootSchema !== void 0 ? { rootSchema } : {}
16
+ });
13
17
  if (typesOnly) {
14
- const imports2 = collectImports(schema, { typesOnly: true, selfRef, rootSchema, typeSuffix, importExt });
18
+ const imports2 = collectImports(schema, {
19
+ typesOnly: true,
20
+ selfRef,
21
+ selfFilename: options?.selfFilename,
22
+ rootSchema,
23
+ typeSuffix,
24
+ importExt
25
+ });
15
26
  let result2 = "";
16
27
  if (imports2.length > 0) {
17
28
  result2 += imports2[0];
@@ -23,7 +34,12 @@ const generateFile = (schema, typeName, options) => {
23
34
  return { content: result2 + typeDefinition, usedHelpers: /* @__PURE__ */ new Set() };
24
35
  }
25
36
  const stripUnknown = options?.stripUnknown ?? false;
26
- const reservedNames = collectImportTypeNames(schema, { selfRef, rootSchema, typeSuffix });
37
+ const reservedNames = collectImportTypeNames(schema, {
38
+ selfRef,
39
+ selfFilename: options?.selfFilename,
40
+ rootSchema,
41
+ typeSuffix
42
+ });
27
43
  const shapeValidator = generateShapeValidator(schema, typeName, true, typeSuffix, true, stripUnknown, reservedNames);
28
44
  const parserFunction = generateParserFunction(schema, typeName, {
29
45
  useRefImports: true,
@@ -41,7 +57,10 @@ const generateFile = (schema, typeName, options) => {
41
57
 
42
58
  ${parserFunction}`;
43
59
  const helpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix, importExt);
44
- const imports = [...collectImports(schema, { selfRef, rootSchema, typeSuffix, importExt }), ...helpers.imports];
60
+ const imports = [
61
+ ...collectImports(schema, { selfRef, selfFilename: options?.selfFilename, rootSchema, typeSuffix, importExt }),
62
+ ...helpers.imports
63
+ ];
45
64
  let result = "";
46
65
  if (imports.length > 0) {
47
66
  result += imports[0];
@@ -10,7 +10,7 @@ import { getDefaultValue } from "../helpers/get-default-value.js";
10
10
  import { getDiscriminatorValue } from "../helpers/get-discriminator-value.js";
11
11
  import { assertNoUnsupportedKeywords } from "./assert-supported-keywords.js";
12
12
  import { generateEnumCaseInsensitiveCoercion } from "./generate-enum-check.js";
13
- import { generateContainsCheck, generateObjectKeywordChecks, generateObjectStrictAssertion, generateScalarStrictAssertion } from "./generate-strict-assertion.js";
13
+ import { generateContainsCheck, generateObjectKeywordChecks, generateObjectStrictAssertion, generateScalarStrictAssertion, inlineAllOfMembers } from "./generate-strict-assertion.js";
14
14
  import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName } from "./generate-type-checks.js";
15
15
  import { generatePrefixItemsMap, generateValidationExpression, getPrefixItems, isCoercibleItemSchema, prefixItemsCapsLength } from "./generate-validation-expression.js";
16
16
  const generateParserName = (typeName) => {
@@ -165,7 +165,8 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
165
165
  if (fallbackValue === "undefined") {
166
166
  return `{} as ${typeName}`;
167
167
  }
168
- requiredProps.push(` ${safeLiteralKey(key)}: ${fallbackValue},`);
168
+ const partialFallback = fallbackValue === "{}" || isSchemaObject(propSchema) && (hasAllOf(propSchema) || hasOneOf(propSchema) || hasAnyOf(propSchema));
169
+ requiredProps.push(` ${safeLiteralKey(key)}: ${partialFallback ? `${fallbackValue} as NonNullable<${typeName}>[${JSON.stringify(key)}]` : fallbackValue},`);
169
170
  }
170
171
  }
171
172
  if (requiredProps.length === 0) {
@@ -330,7 +331,7 @@ ${delegatedBody(itemParserName)}
330
331
  const stripUnknown = unionCtx?.stripUnknown ?? false;
331
332
  const itemShapeValidator = generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved);
332
333
  const preamble = [
333
- `type ${itemName} = ${typeName}[number];`,
334
+ `type ${itemName} = NonNullable<${typeName}>[number];`,
334
335
  itemShapeValidator,
335
336
  generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved, unionCtx?.caseInsensitive ?? false, itemShapeValidator)
336
337
  ].join("\n\n");
@@ -339,7 +340,8 @@ ${delegatedBody(itemParserName)}
339
340
  ${delegated(generateParserName(itemName))}`;
340
341
  }
341
342
  }
342
- if (!isSchemaObject(schema) || !hasType(schema)) {
343
+ const isMultiType = isSchemaObject(schema) && Array.isArray(schema.type);
344
+ if (!isSchemaObject(schema) || !hasType(schema) && !isMultiType) {
343
345
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
344
346
  }
345
347
  if (!strict && schema.type === "null") {
@@ -350,7 +352,8 @@ ${delegated(generateParserName(itemName))}`;
350
352
  if (assertion === null) {
351
353
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
352
354
  }
353
- const returnExpr = schema.type === "array" ? `[...(input as readonly unknown[])] as ${typeName}` : `input as ${typeName}`;
355
+ const arrayCast = getPrefixItems(schema) ? `[...(input as readonly unknown[])] as unknown as ${typeName}` : `[...(input as readonly unknown[])] as ${typeName}`;
356
+ const returnExpr = !isMultiType && schema.type === "array" ? arrayCast : `input as ${typeName}`;
354
357
  return `export const ${functionName} = (input: unknown): ${typeName} => {
355
358
  ${assertion}
356
359
  return ${returnExpr};
@@ -369,7 +372,7 @@ ${assertion}
369
372
  const prefixItems = getPrefixItems(schema);
370
373
  if (prefixItems) {
371
374
  const mapped = generatePrefixItemsMap("input", prefixItems, prefixItemsCapsLength(schema), unionCtx?.rootSchema, void 0, unionCtx?.caseInsensitive);
372
- return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? ${mapped} as ${typeName} : [] as ${typeName};`;
375
+ return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? ${mapped} as unknown as ${typeName} : [] as unknown as ${typeName};`;
373
376
  }
374
377
  if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
375
378
  const item = schema.items;
@@ -485,7 +488,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
485
488
  const preamble = [];
486
489
  for (const [key, subName] of subTypeNames) {
487
490
  const propSchema = schemaProps[key];
488
- preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
491
+ preamble.push(`type ${subName} = NonNullable<${typeName}>[${JSON.stringify(key)}];`);
489
492
  const subShapeValidator = generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames);
490
493
  preamble.push(subShapeValidator);
491
494
  preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames, caseInsensitive, subShapeValidator));
@@ -493,7 +496,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
493
496
  for (const [key, subName] of subItemNames) {
494
497
  const propSchema = schemaProps[key];
495
498
  const itemSchema = propSchema.items;
496
- preamble.push(`type ${subName} = NonNullable<${typeName}[${JSON.stringify(key)}]>[number];`);
499
+ preamble.push(`type ${subName} = NonNullable<NonNullable<${typeName}>[${JSON.stringify(key)}]>[number];`);
497
500
  const itemShapeValidator = generateShapeValidator(itemSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames);
498
501
  preamble.push(itemShapeValidator);
499
502
  preamble.push(`const _every${subName} = (arr: readonly unknown[]): boolean => {
@@ -522,7 +525,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
522
525
  const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix, subTypeNames);
523
526
  const propInfo = [];
524
527
  const hasAllOfRefParsers = useRefImports && isSchemaObject(schema) && hasAllOf(schema) && schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
525
- let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema);
528
+ let canFastPath = !hasAllOfRefParsers && !hasFastPathBlockingKeyword(schema) && !(strict && inlineAllOfMembers(schema).length > 0);
526
529
  const fastPathChecks = [];
527
530
  const fastPathAccessorChecks = [];
528
531
  const usedVarNames = /* @__PURE__ */ new Set();
@@ -46,6 +46,13 @@ export declare const generateObjectKeywordChecks: (obj: string, schema: JSONSche
46
46
  * Properties with a `$ref` are validated by the nested parser's own strict
47
47
  * check when that parser is invoked downstream.
48
48
  */
49
+ /**
50
+ * The `allOf` members this assertion must enforce itself: plain object schemas
51
+ * written inline. A `$ref` member is enforced by the parser generated for its
52
+ * target, and a member that composes further (nested `allOf`/`oneOf`/`if`) is
53
+ * left alone rather than half-checked.
54
+ */
55
+ export declare const inlineAllOfMembers: (schema: JSONSchema) => JSONSchema[];
49
56
  export declare const generateObjectStrictAssertion: (schema: JSONSchema, typeName: string, context?: StrictAssertionContext) => string[];
50
57
  /**
51
58
  * Generates a single strict-mode assertion line for a non-object scalar parser.
@@ -6,7 +6,7 @@ import { resolveRef } from "@amritk/helpers/resolve-ref";
6
6
  import { safeAccessor } from "@amritk/helpers/safe-accessor";
7
7
  import { hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
8
8
  import { generateEnumCheck } from "./generate-enum-check.js";
9
- import { canEnforceUnion, generateUnionCheck, getUnionBranches, isInlineObjectProperty } from "./generate-type-checks.js";
9
+ import { canEnforceUnion, generateUnionCheck, getUnionBranches, isInlineObjectProperty, multiTypeCheck } from "./generate-type-checks.js";
10
10
  import { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from "./generate-validation-expression.js";
11
11
  import { subschemaMatchExpr } from "./subschema-match.js";
12
12
  const wrongTypeCondition = (accessor, type) => {
@@ -262,6 +262,19 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
262
262
  }
263
263
  return lines;
264
264
  }
265
+ const multiType = multiTypeCheck(acc, propSchema, { ignoreConstraints: true });
266
+ if (multiType !== void 0) {
267
+ const types = propSchema.type;
268
+ if (multiType !== null) {
269
+ const expected = throwError(`${field} expected ${types.map(typeLabel).join(" | ")}, got `, `typeof ${acc}`);
270
+ lines.push(isRequired ? ` if (!${multiType}) ${expected};` : ` if (${acc} !== undefined && !${multiType}) ${expected};`);
271
+ }
272
+ const nonNull = types.filter((type) => type !== "null");
273
+ if (nonNull.length === 1) {
274
+ lines.push(...generateConstraintChecks(acc, { ...propSchema, type: nonNull[0] }, field, context));
275
+ }
276
+ return lines;
277
+ }
265
278
  if (hasType(propSchema)) {
266
279
  const t = propSchema.type;
267
280
  const wrongType = wrongTypeCondition(acc, t);
@@ -285,17 +298,39 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
285
298
  }
286
299
  return lines;
287
300
  };
301
+ const inlineAllOfMembers = (schema) => {
302
+ if (!isSchemaObject(schema) || !Array.isArray(schema.allOf))
303
+ return [];
304
+ return schema.allOf.filter((member) => {
305
+ if (!isSchemaObject(member) || hasRef(member))
306
+ return false;
307
+ const record = member;
308
+ if ("allOf" in record || "oneOf" in record || "anyOf" in record || "not" in record)
309
+ return false;
310
+ if ("if" in record || "then" in record || "else" in record)
311
+ return false;
312
+ return hasProperties(member) || hasRequired(member);
313
+ });
314
+ };
288
315
  const generateObjectStrictAssertion = (schema, typeName, context = {}) => {
289
316
  const lines = [];
290
317
  lines.push(` if (!isObject(input)) ${throwError(`[${typeName}] expected object, got `, 'input === null ? "null" : typeof input')};`);
291
318
  if (!isSchemaObject(schema))
292
319
  return lines;
320
+ const required = new Set(hasRequired(schema) ? schema.required : []);
293
321
  if (hasProperties(schema)) {
294
- const required = new Set(hasRequired(schema) ? schema.required : []);
295
322
  const props = schema.properties;
296
323
  for (const key in props) {
297
324
  lines.push(...generatePropertyAssertion(key, props[key], required.has(key), typeName, context));
298
325
  }
326
+ for (const key in props)
327
+ required.delete(key);
328
+ }
329
+ for (const key of required) {
330
+ lines.push(` if (!(${JSON.stringify(key)} in input)) ${throwError(`[${typeName}] missing required property '${key}'`)};`);
331
+ }
332
+ for (const member of inlineAllOfMembers(schema)) {
333
+ lines.push(...generateObjectStrictAssertion(member, typeName, context).slice(1));
299
334
  }
300
335
  const sr = schema;
301
336
  if ("dependentRequired" in sr || "dependentSchemas" in sr || "propertyNames" in sr) {
@@ -325,6 +360,18 @@ const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
325
360
  const enumLabel = schema.enum.map((v) => JSON.stringify(v)).join(", ");
326
361
  lines.push(` if (!(${allowed} as readonly unknown[]).includes(input)) ${throwError(`${label} must be one of: ${enumLabel}`)};`);
327
362
  }
363
+ const rootMultiType = multiTypeCheck("input", schema, { ignoreConstraints: true });
364
+ if (rootMultiType !== void 0) {
365
+ const types = schema.type;
366
+ if (rootMultiType !== null) {
367
+ lines.push(` if (!${rootMultiType}) ${throwError(`${label} expected ${types.map(typeLabel).join(" | ")}, got `, got)};`);
368
+ }
369
+ const nonNull = types.filter((type) => type !== "null");
370
+ if (nonNull.length === 1) {
371
+ lines.push(...generateConstraintChecks("input", { ...schema, type: nonNull[0] }, label, rootSchema ? { rootSchema } : {}));
372
+ }
373
+ return lines.length > 0 ? lines.join("\n") : null;
374
+ }
328
375
  if (hasType(schema)) {
329
376
  const t = schema.type;
330
377
  const wrongType = wrongTypeCondition("input", t);
@@ -332,12 +379,7 @@ const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
332
379
  lines.push(` if (${wrongType}) ${throwError(`${label} expected ${typeLabel(t)}, got `, got)};`);
333
380
  }
334
381
  if (t === "array") {
335
- const itemCheck = generateItemCheck(schema);
336
- if (itemCheck) {
337
- lines.push(` if (!(input as readonly unknown[]).every((_it) => ${itemCheck.check})) ${throwError(`${label} ${itemCheck.message}`)};`);
338
- }
339
382
  lines.push(...generateContainsCheck("input", schema, label));
340
- lines.push(...generatePrefixItemsAssertion("input", label, schema, rootSchema));
341
383
  }
342
384
  lines.push(...generateConstraintChecks("input", schema, label, rootSchema ? { rootSchema } : {}));
343
385
  }
@@ -347,5 +389,6 @@ export {
347
389
  generateContainsCheck,
348
390
  generateObjectKeywordChecks,
349
391
  generateObjectStrictAssertion,
350
- generateScalarStrictAssertion
392
+ generateScalarStrictAssertion,
393
+ inlineAllOfMembers
351
394
  };
@@ -50,6 +50,21 @@ export declare const getUnionBranches: (schema: JSONSchema) => readonly JSONSche
50
50
  * both must bail when this is true — the slow path then runs the real checks.
51
51
  */
52
52
  export declare const hasFastPathBlockingKeyword: (schema: JSONSchema) => boolean;
53
+ /**
54
+ * The membership check for an array-form `type` (the multi-type / nullable
55
+ * idiom, e.g. `["string","null"]`): the disjunction of the per-type checks.
56
+ * Returns `undefined` when the schema has no array `type`, and `null` when it
57
+ * has one that cannot be checked soundly — a type name we do not model, or a
58
+ * constraint keyword a bare `typeof` would skip.
59
+ *
60
+ * Without this every nullable property fell through to `hasType`, which is false
61
+ * for an array `type`, and produced no check at all: no fast path (so the shape
62
+ * validator degraded to the `=> false` stub) and, in strict mode, nothing to
63
+ * throw on.
64
+ */
65
+ export declare const multiTypeCheck: (varName: string, schema: JSONSchema, options?: {
66
+ readonly ignoreConstraints?: boolean;
67
+ }) => string | null | undefined;
53
68
  /**
54
69
  * Generates a fast-path type check expression for a property.
55
70
  * Returns null if the schema is too complex for a simple type check.
@@ -5,13 +5,15 @@ import { resolveRef } from "@amritk/helpers/resolve-ref";
5
5
  import { safeAccessor } from "@amritk/helpers/safe-accessor";
6
6
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
7
7
  import { generateEnumCheck } from "./generate-enum-check.js";
8
- import { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from "./generate-validation-expression.js";
8
+ import { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck, singleTypeCheck } from "./generate-validation-expression.js";
9
9
  const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
10
10
  const isInlineObjectProperty = (propSchema) => {
11
11
  if (!isSchemaObject(propSchema))
12
12
  return false;
13
13
  if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema))
14
14
  return false;
15
+ if (Array.isArray(propSchema.type))
16
+ return false;
15
17
  if (hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema))
16
18
  return false;
17
19
  if ("patternProperties" in propSchema || "not" in propSchema)
@@ -48,6 +50,44 @@ const hasFastPathBlockingKeyword = (schema) => {
48
50
  const record = schema;
49
51
  return "contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record;
50
52
  };
53
+ const CONSTRAINT_KEYWORDS = [
54
+ "pattern",
55
+ "minLength",
56
+ "maxLength",
57
+ "minimum",
58
+ "maximum",
59
+ "exclusiveMinimum",
60
+ "exclusiveMaximum",
61
+ "multipleOf",
62
+ "minItems",
63
+ "maxItems",
64
+ "uniqueItems",
65
+ "items",
66
+ "prefixItems",
67
+ "properties",
68
+ "required",
69
+ "additionalProperties",
70
+ "patternProperties",
71
+ "minProperties",
72
+ "maxProperties"
73
+ ];
74
+ const multiTypeCheck = (varName, schema, options = {}) => {
75
+ if (!isSchemaObject(schema) || !Array.isArray(schema.type))
76
+ return void 0;
77
+ const record = schema;
78
+ if (!options.ignoreConstraints && CONSTRAINT_KEYWORDS.some((keyword) => keyword in record))
79
+ return null;
80
+ const checks = [];
81
+ for (const type of schema.type) {
82
+ const check = singleTypeCheck(varName, type);
83
+ if (check === null)
84
+ return null;
85
+ checks.push(`(${check})`);
86
+ }
87
+ if (checks.length === 0)
88
+ return null;
89
+ return checks.length === 1 ? checks[0] : `(${checks.join(" || ")})`;
90
+ };
51
91
  const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
52
92
  if (!isSchemaObject(schema))
53
93
  return null;
@@ -75,6 +115,9 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
75
115
  const c = schema.const;
76
116
  return c === null || typeof c !== "object" ? `${varName} === ${JSON.stringify(c)}` : null;
77
117
  }
118
+ const multiType = multiTypeCheck(varName, schema);
119
+ if (multiType !== void 0)
120
+ return multiType;
78
121
  if (!hasType(schema))
79
122
  return null;
80
123
  if (useRefImports && schema.type === "array" && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
@@ -225,6 +268,9 @@ const canTrustPropertyCheck = (schema, rootSchema, visiting) => {
225
268
  return false;
226
269
  if (hasConst(schema))
227
270
  return schema.const === null || typeof schema.const !== "object";
271
+ const multiType = multiTypeCheck("_x", schema);
272
+ if (multiType !== void 0)
273
+ return multiType !== null;
228
274
  if (!hasType(schema))
229
275
  return false;
230
276
  if (schema.type === "array" && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
@@ -309,5 +355,6 @@ export {
309
355
  hasFastPathBlockingKeyword,
310
356
  isInlineObjectArrayProperty,
311
357
  isInlineObjectProperty,
358
+ multiTypeCheck,
312
359
  shapeValidatorName
313
360
  };
@@ -14,10 +14,11 @@ export declare const scalarItemTypeCheck: (itemSchema: JSONSchema, accessor: str
14
14
  */
15
15
  export declare const isCoercibleItemSchema: (itemSchema: JSONSchema) => boolean;
16
16
  /**
17
- * The tuple `prefixItems` array when the schema declares one, else `null`.
18
- * `prefixItems` (JSON Schema 2020-12) positions each element against its own
19
- * subschema; the draft array-form `items` is handled by the existing item
20
- * paths, not here.
17
+ * The tuple positions a schema declares, else `null`. `prefixItems` (JSON
18
+ * Schema 2020-12) positions each element against its own subschema; draft-07
19
+ * spelled the same construct as an array-valued `items`, which every other item
20
+ * path skips (they all read `items` as a single schema), so it is normalized
21
+ * here rather than left unchecked.
21
22
  */
22
23
  export declare const getPrefixItems: (schema: JSONSchema) => readonly JSONSchema[] | null;
23
24
  /**
@@ -26,6 +27,14 @@ export declare const getPrefixItems: (schema: JSONSchema) => readonly JSONSchema
26
27
  * disallowed, so the coercer drops it and strict mode rejects it.
27
28
  */
28
29
  export declare const prefixItemsCapsLength: (schema: JSONSchema) => boolean;
30
+ /**
31
+ * A boolean type check for a single JSON Schema primitive type name. Unlike
32
+ * {@link scalarItemTypeCheck} this also covers `array`/`object` (as inline shape
33
+ * checks, so no `isObject` import is assumed) and enforces `integer` with
34
+ * `Number.isInteger` — a bare `typeof === "number"` accepts `1.5`. Used to build
35
+ * the disjunction for an array-form `type` (e.g. `["string","null"]`).
36
+ */
37
+ export declare const singleTypeCheck: (accessor: string, type: string) => string | null;
29
38
  /**
30
39
  * Generates a validation expression that checks if a value matches the expected type.
31
40
  * Returns the value if valid, coerces to the correct type if invalid, or uses default if required and missing.
@@ -42,7 +42,9 @@ const getPrefixItems = (schema) => {
42
42
  if (!isSchemaObject(schema))
43
43
  return null;
44
44
  const prefix = schema["prefixItems"];
45
- return Array.isArray(prefix) ? prefix : null;
45
+ if (Array.isArray(prefix))
46
+ return prefix;
47
+ return Array.isArray(schema.items) ? schema.items : null;
46
48
  };
47
49
  const prefixItemsCapsLength = (schema) => {
48
50
  if (!isSchemaObject(schema))
@@ -328,5 +330,6 @@ export {
328
330
  getPrefixItems,
329
331
  isCoercibleItemSchema,
330
332
  prefixItemsCapsLength,
331
- scalarItemTypeCheck
333
+ scalarItemTypeCheck,
334
+ singleTypeCheck
332
335
  };
@@ -16,6 +16,14 @@ type CollectImportsOptions = {
16
16
  * the import list, preventing a file from importing itself.
17
17
  */
18
18
  readonly selfRef?: string | undefined;
19
+ /**
20
+ * The filename this file is being written as, without extension. Takes
21
+ * precedence over `selfRef` for self-import detection, and covers the case
22
+ * `selfRef` cannot: the root document has no ref of its own, so a definition
23
+ * whose name collides with the root type name (root `Contact` + `$defs.contact`)
24
+ * used to make the root file import its own type from itself.
25
+ */
26
+ readonly selfFilename?: string | undefined;
19
27
  /**
20
28
  * The root schema document. When provided, URI refs that cannot be resolved
21
29
  * within the root schema's $defs are excluded from the import list, preventing
@@ -21,7 +21,7 @@ const collectImportTypeNames = (schema, options) => {
21
21
  return names;
22
22
  };
23
23
  const collectImportTargets = (schema, options) => {
24
- const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : void 0;
24
+ const selfFilename = options?.selfFilename ?? (options?.selfRef ? refToFilename(options.selfRef) : void 0);
25
25
  const rootSchema = options?.rootSchema;
26
26
  const typeSuffix = options?.typeSuffix;
27
27
  const refs = /* @__PURE__ */ new Set();
@@ -1,11 +1,33 @@
1
1
  import { getMjstPrimitive } from "@amritk/helpers/mjst-extension";
2
2
  import { hasAllOf, hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasOneOf, hasPattern, hasProperties, hasRequired, hasType, isSchemaObject } from "@amritk/helpers/schema-guards";
3
3
  import { generateDefaultFromPattern } from "../generators/generate-default-from-pattern.js";
4
+ const defaultMatchesType = (value, schema) => {
5
+ if (!hasType(schema))
6
+ return true;
7
+ switch (schema.type) {
8
+ case "string":
9
+ return typeof value === "string";
10
+ case "number":
11
+ return typeof value === "number";
12
+ case "integer":
13
+ return typeof value === "number" && Number.isInteger(value);
14
+ case "boolean":
15
+ return typeof value === "boolean";
16
+ case "null":
17
+ return value === null;
18
+ case "array":
19
+ return Array.isArray(value);
20
+ case "object":
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ default:
23
+ return true;
24
+ }
25
+ };
4
26
  const getDefaultValue = (schema) => {
5
27
  if (!isSchemaObject(schema)) {
6
28
  return "undefined";
7
29
  }
8
- if (hasDefault(schema)) {
30
+ if (hasDefault(schema) && defaultMatchesType(schema.default, schema)) {
9
31
  return JSON.stringify(schema.default);
10
32
  }
11
33
  if (hasConst(schema)) {
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "amritk",
9
13
  "keywords": [
@@ -49,8 +53,8 @@
49
53
  },
50
54
  "dependencies": {
51
55
  "json-schema-typed": "^8.0.1",
52
- "@amritk/helpers": "0.13.4",
53
- "@amritk/generate-markdown": "0.4.4"
56
+ "@amritk/helpers": "0.14.0",
57
+ "@amritk/generate-markdown": "0.4.5"
54
58
  },
55
59
  "devDependencies": {
56
60
  "@sinclair/typebox": "^0.34.49",