@gabrielbryk/json-schema-to-zod 2.10.0 → 2.11.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.
Files changed (138) hide show
  1. package/AGENTS.md +44 -0
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +6 -33
  4. package/check-types-lift.sh +23 -0
  5. package/check-types.sh +20 -0
  6. package/dist/{esm/cli.js → cli.js} +0 -6
  7. package/dist/{esm/core → core}/analyzeSchema.js +4 -5
  8. package/dist/core/emitZod.js +263 -0
  9. package/dist/{esm/generators → generators}/generateBundle.js +225 -67
  10. package/dist/{esm/index.js → index.js} +6 -0
  11. package/dist/jsonSchemaToZod.js +17 -0
  12. package/dist/parsers/parseAllOf.js +125 -0
  13. package/dist/parsers/parseAnyOf.js +28 -0
  14. package/dist/{esm/parsers → parsers}/parseArray.js +27 -11
  15. package/dist/parsers/parseBoolean.js +4 -0
  16. package/dist/parsers/parseConst.js +22 -0
  17. package/dist/parsers/parseEnum.js +35 -0
  18. package/dist/{esm/parsers → parsers}/parseIfThenElse.js +11 -7
  19. package/dist/parsers/parseMultipleType.js +10 -0
  20. package/dist/parsers/parseNot.js +14 -0
  21. package/dist/parsers/parseNull.js +4 -0
  22. package/dist/parsers/parseNullable.js +12 -0
  23. package/dist/{esm/parsers → parsers}/parseNumber.js +4 -1
  24. package/dist/{esm/parsers → parsers}/parseObject.js +168 -29
  25. package/dist/parsers/parseOneOf.js +365 -0
  26. package/dist/{esm/parsers → parsers}/parseSchema.js +56 -110
  27. package/dist/parsers/parseSimpleDiscriminatedOneOf.js +24 -0
  28. package/dist/{esm/parsers → parsers}/parseString.js +29 -18
  29. package/dist/types/Types.d.ts +32 -4
  30. package/dist/types/core/analyzeSchema.d.ts +3 -2
  31. package/dist/types/generators/generateBundle.d.ts +0 -2
  32. package/dist/types/index.d.ts +6 -0
  33. package/dist/types/parsers/parseAllOf.d.ts +2 -2
  34. package/dist/types/parsers/parseAnyOf.d.ts +2 -2
  35. package/dist/types/parsers/parseArray.d.ts +2 -2
  36. package/dist/types/parsers/parseBoolean.d.ts +2 -1
  37. package/dist/types/parsers/parseConst.d.ts +2 -2
  38. package/dist/types/parsers/parseDefault.d.ts +2 -2
  39. package/dist/types/parsers/parseEnum.d.ts +2 -2
  40. package/dist/types/parsers/parseIfThenElse.d.ts +2 -2
  41. package/dist/types/parsers/parseMultipleType.d.ts +2 -2
  42. package/dist/types/parsers/parseNot.d.ts +2 -2
  43. package/dist/types/parsers/parseNull.d.ts +2 -1
  44. package/dist/types/parsers/parseNullable.d.ts +2 -2
  45. package/dist/types/parsers/parseNumber.d.ts +2 -2
  46. package/dist/types/parsers/parseObject.d.ts +2 -2
  47. package/dist/types/parsers/parseOneOf.d.ts +2 -2
  48. package/dist/types/parsers/parseSchema.d.ts +2 -2
  49. package/dist/types/parsers/parseSimpleDiscriminatedOneOf.d.ts +2 -2
  50. package/dist/types/parsers/parseString.d.ts +2 -2
  51. package/dist/types/utils/anyOrUnknown.d.ts +5 -4
  52. package/dist/types/utils/esmEmitter.d.ts +29 -0
  53. package/dist/types/utils/extractInlineObject.d.ts +15 -0
  54. package/dist/types/utils/liftInlineObjects.d.ts +21 -0
  55. package/dist/types/utils/namingService.d.ts +21 -0
  56. package/dist/types/utils/resolveRef.d.ts +7 -0
  57. package/dist/types/utils/schemaRepresentation.d.ts +71 -0
  58. package/dist/utils/anyOrUnknown.js +13 -0
  59. package/dist/{esm/utils → utils}/buildRefRegistry.js +4 -0
  60. package/dist/utils/esmEmitter.js +87 -0
  61. package/dist/utils/extractInlineObject.js +119 -0
  62. package/dist/utils/liftInlineObjects.js +476 -0
  63. package/dist/utils/namingService.js +58 -0
  64. package/dist/utils/resolveRef.js +92 -0
  65. package/dist/utils/schemaRepresentation.js +569 -0
  66. package/docs/IMPROVEMENT-PLAN.md +243 -0
  67. package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +292 -0
  68. package/docs/proposals/bundle-refactor.md +1 -1
  69. package/docs/proposals/discriminated-union-with-default.md +248 -0
  70. package/docs/proposals/inline-object-lifting.md +77 -0
  71. package/eslint.config.js +4 -2
  72. package/jest.config.mjs +19 -0
  73. package/package.json +17 -20
  74. package/scripts/generateWorkflowSchema.ts +0 -1
  75. package/dist/cjs/Types.js +0 -2
  76. package/dist/cjs/cli.js +0 -70
  77. package/dist/cjs/core/analyzeSchema.js +0 -62
  78. package/dist/cjs/core/emitZod.js +0 -141
  79. package/dist/cjs/generators/generateBundle.js +0 -365
  80. package/dist/cjs/index.js +0 -50
  81. package/dist/cjs/jsonSchemaToZod.js +0 -10
  82. package/dist/cjs/package.json +0 -1
  83. package/dist/cjs/parsers/parseAllOf.js +0 -46
  84. package/dist/cjs/parsers/parseAnyOf.js +0 -18
  85. package/dist/cjs/parsers/parseArray.js +0 -90
  86. package/dist/cjs/parsers/parseBoolean.js +0 -5
  87. package/dist/cjs/parsers/parseConst.js +0 -7
  88. package/dist/cjs/parsers/parseDefault.js +0 -8
  89. package/dist/cjs/parsers/parseEnum.js +0 -21
  90. package/dist/cjs/parsers/parseIfThenElse.js +0 -35
  91. package/dist/cjs/parsers/parseMultipleType.js +0 -10
  92. package/dist/cjs/parsers/parseNot.js +0 -12
  93. package/dist/cjs/parsers/parseNull.js +0 -5
  94. package/dist/cjs/parsers/parseNullable.js +0 -12
  95. package/dist/cjs/parsers/parseNumber.js +0 -116
  96. package/dist/cjs/parsers/parseObject.js +0 -315
  97. package/dist/cjs/parsers/parseOneOf.js +0 -53
  98. package/dist/cjs/parsers/parseSchema.js +0 -411
  99. package/dist/cjs/parsers/parseSimpleDiscriminatedOneOf.js +0 -21
  100. package/dist/cjs/parsers/parseString.js +0 -317
  101. package/dist/cjs/utils/anyOrUnknown.js +0 -14
  102. package/dist/cjs/utils/buildRefRegistry.js +0 -56
  103. package/dist/cjs/utils/cliTools.js +0 -108
  104. package/dist/cjs/utils/cycles.js +0 -113
  105. package/dist/cjs/utils/half.js +0 -7
  106. package/dist/cjs/utils/jsdocs.js +0 -20
  107. package/dist/cjs/utils/omit.js +0 -11
  108. package/dist/cjs/utils/resolveUri.js +0 -16
  109. package/dist/cjs/utils/withMessage.js +0 -21
  110. package/dist/cjs/zodToJsonSchema.js +0 -89
  111. package/dist/esm/core/emitZod.js +0 -137
  112. package/dist/esm/jsonSchemaToZod.js +0 -6
  113. package/dist/esm/package.json +0 -1
  114. package/dist/esm/parsers/parseAllOf.js +0 -43
  115. package/dist/esm/parsers/parseAnyOf.js +0 -14
  116. package/dist/esm/parsers/parseBoolean.js +0 -1
  117. package/dist/esm/parsers/parseConst.js +0 -3
  118. package/dist/esm/parsers/parseEnum.js +0 -17
  119. package/dist/esm/parsers/parseMultipleType.js +0 -6
  120. package/dist/esm/parsers/parseNot.js +0 -8
  121. package/dist/esm/parsers/parseNull.js +0 -1
  122. package/dist/esm/parsers/parseNullable.js +0 -8
  123. package/dist/esm/parsers/parseOneOf.js +0 -49
  124. package/dist/esm/parsers/parseSimpleDiscriminatedOneOf.js +0 -17
  125. package/dist/esm/utils/anyOrUnknown.js +0 -10
  126. package/jest.config.cjs +0 -4
  127. package/postcjs.cjs +0 -1
  128. package/postesm.cjs +0 -1
  129. /package/dist/{esm/Types.js → Types.js} +0 -0
  130. /package/dist/{esm/parsers → parsers}/parseDefault.js +0 -0
  131. /package/dist/{esm/utils → utils}/cliTools.js +0 -0
  132. /package/dist/{esm/utils → utils}/cycles.js +0 -0
  133. /package/dist/{esm/utils → utils}/half.js +0 -0
  134. /package/dist/{esm/utils → utils}/jsdocs.js +0 -0
  135. /package/dist/{esm/utils → utils}/omit.js +0 -0
  136. /package/dist/{esm/utils → utils}/resolveUri.js +0 -0
  137. /package/dist/{esm/utils → utils}/withMessage.js +0 -0
  138. /package/dist/{esm/zodToJsonSchema.js → zodToJsonSchema.js} +0 -0
@@ -1,36 +1,50 @@
1
1
  import { analyzeSchema } from "../core/analyzeSchema.js";
2
2
  import { emitZod } from "../core/emitZod.js";
3
+ import { liftInlineObjects } from "../utils/liftInlineObjects.js";
3
4
  export const generateSchemaBundle = (schema, options = {}) => {
4
- const module = options.module ?? "esm";
5
5
  if (!schema || typeof schema !== "object") {
6
6
  throw new Error("generateSchemaBundle requires an object schema");
7
7
  }
8
- const defs = schema.$defs || {};
9
- const definitions = schema.definitions || {};
8
+ const liftOpts = options.liftInlineObjects ?? {};
9
+ const useLift = liftOpts.enable !== false;
10
+ const liftedSchema = useLift
11
+ ? liftInlineObjects(schema, {
12
+ enable: true,
13
+ nameForPath: liftOpts.nameForPath,
14
+ parentName: options.splitDefs?.rootTypeName ?? options.splitDefs?.rootName ?? schema.title,
15
+ dedup: liftOpts.dedup === true,
16
+ allowInDefs: liftOpts.allowInDefs,
17
+ }).schema
18
+ : schema;
19
+ const defs = liftedSchema.$defs || {};
20
+ const definitions = liftedSchema.definitions || {};
10
21
  const defNames = Object.keys(defs);
11
22
  const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
12
23
  const files = [];
13
- const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, rootTypeName);
24
+ const targets = planBundleTargets(liftedSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
14
25
  for (const target of targets) {
15
26
  const usedRefs = target.usedRefs;
16
- const analysis = analyzeSchema(target.schemaWithDefs, {
17
- ...options,
18
- module,
19
- name: target.schemaName,
20
- type: target.typeName,
21
- parserOverride: createRefHandler(target.defName, defInfoMap, usedRefs, {
22
- ...(target.schemaWithDefs.$defs || {}),
23
- ...(target.schemaWithDefs.definitions || {}),
24
- }, options),
25
- });
26
- const zodSchema = emitZod(analysis);
27
- const finalSchema = buildSchemaFile(zodSchema, usedRefs, defInfoMap, module);
27
+ const zodParts = [];
28
+ for (const member of target.members) {
29
+ const analysis = analyzeSchema(member.schemaWithDefs, {
30
+ ...options,
31
+ name: member.schemaName,
32
+ type: member.typeName,
33
+ parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, {
34
+ ...(member.schemaWithDefs.$defs || {}),
35
+ ...(member.schemaWithDefs.definitions || {}),
36
+ }, options, target.groupId),
37
+ });
38
+ const zodSchema = emitZod(analysis);
39
+ zodParts.push(zodSchema);
40
+ }
41
+ const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap);
28
42
  files.push({ fileName: target.fileName, contents: finalSchema });
29
43
  }
30
44
  // Nested types extraction (optional)
31
45
  const nestedTypesEnabled = options.nestedTypes?.enable;
32
46
  if (nestedTypesEnabled) {
33
- const nestedTypes = collectNestedTypes(schema, defs, defNames, rootTypeName ?? rootName);
47
+ const nestedTypes = collectNestedTypes(liftedSchema, defs, defNames, rootTypeName ?? rootName);
34
48
  if (nestedTypes.length > 0) {
35
49
  const nestedFileName = options.nestedTypes?.fileName ?? "nested-types.ts";
36
50
  const nestedContent = generateNestedTypesFile(nestedTypes);
@@ -53,13 +67,16 @@ const buildDefInfoMap = (defNames, defs, options) => {
53
67
  const pascalName = toPascalCase(defName);
54
68
  const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
55
69
  const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
70
+ const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
56
71
  map.set(defName, {
57
72
  name: defName,
58
73
  pascalName,
59
74
  schemaName,
60
75
  typeName,
76
+ fileName,
61
77
  dependencies,
62
78
  hasCycle: false,
79
+ groupId: "",
63
80
  });
64
81
  }
65
82
  return map;
@@ -72,13 +89,21 @@ const buildBundleContext = (defNames, defs, options) => {
72
89
  if (info)
73
90
  info.hasCycle = true;
74
91
  }
92
+ const groups = buildSccGroups(defInfoMap);
93
+ for (const [groupId, members] of groups) {
94
+ for (const defName of members) {
95
+ const info = defInfoMap.get(defName);
96
+ if (info)
97
+ info.groupId = groupId;
98
+ }
99
+ }
75
100
  const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
76
101
  const rootTypeName = typeof options.type === "string"
77
102
  ? options.type
78
103
  : options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
79
- return { defInfoMap, rootName, rootTypeName };
104
+ return { defInfoMap, rootName, rootTypeName, groups };
80
105
  };
81
- const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
106
+ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
82
107
  const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
83
108
  return (schema, refs) => {
84
109
  if (typeof schema["$ref"] === "string") {
@@ -93,7 +118,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
93
118
  const refInfo = defInfoMap.get(refName);
94
119
  if (refInfo) {
95
120
  // Track imports when referencing other defs
96
- if (refName !== currentDefName) {
121
+ if (refName !== currentDefName && refInfo.groupId !== currentGroupId) {
97
122
  usedRefs.add(refName);
98
123
  }
99
124
  const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
@@ -107,7 +132,14 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
107
132
  if (resolved)
108
133
  return resolved;
109
134
  if (isCycle && useLazyCrossRefs) {
110
- return `z.lazy(() => ${refInfo.schemaName})`;
135
+ const inObjectProperty = refs.path.includes("properties") ||
136
+ refs.path.includes("patternProperties") ||
137
+ refs.path.includes("additionalProperties");
138
+ if (inObjectProperty && refName === currentDefName) {
139
+ // Self-recursion inside object getters can safely reference the schema name
140
+ return refInfo.schemaName;
141
+ }
142
+ return `z.lazy<typeof ${refInfo.schemaName}>(() => ${refInfo.schemaName})`;
111
143
  }
112
144
  return refInfo.schemaName;
113
145
  }
@@ -125,42 +157,72 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
125
157
  return undefined;
126
158
  };
127
159
  };
128
- const buildSchemaFile = (zodCode, usedRefs, defInfoMap, module) => {
129
- if (module !== "esm")
130
- return zodCode;
131
- const imports = [];
160
+ const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap) => {
161
+ const groupFileById = new Map();
162
+ for (const info of defInfoMap.values()) {
163
+ if (!groupFileById.has(info.groupId)) {
164
+ groupFileById.set(info.groupId, info.fileName.replace(/\.ts$/, ".js"));
165
+ }
166
+ }
167
+ const importsByFile = new Map();
132
168
  for (const refName of [...usedRefs].sort()) {
133
169
  const refInfo = defInfoMap.get(refName);
134
170
  if (refInfo) {
135
- imports.push(`import { ${refInfo.schemaName} } from './${refName}.schema.js';`);
171
+ const groupFile = groupFileById.get(refInfo.groupId) ?? refInfo.fileName.replace(/\.ts$/, ".js");
172
+ const path = `./${groupFile}`;
173
+ const set = importsByFile.get(path) ?? new Set();
174
+ set.add(refInfo.schemaName);
175
+ importsByFile.set(path, set);
136
176
  }
137
177
  }
138
- if (!imports.length)
139
- return zodCode;
140
- return zodCode.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`);
178
+ const imports = [];
179
+ for (const [path, names] of [...importsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
180
+ imports.push(`import { ${[...names].sort().join(", ")} } from '${path}';`);
181
+ }
182
+ const body = zodCodeParts
183
+ .map((code, idx) => {
184
+ if (idx === 0)
185
+ return code;
186
+ return code.replace(/^import \{ z \} from "zod"\n?/, "");
187
+ })
188
+ .join("\n");
189
+ const withImports = imports.length
190
+ ? body.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`)
191
+ : body;
192
+ return withImports;
141
193
  };
142
- const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, rootTypeName) => {
194
+ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
143
195
  const targets = [];
196
+ const groupById = new Map();
144
197
  for (const defName of defNames) {
145
- const defSchema = defs[defName];
146
- const defSchemaWithDefs = {
147
- ...defSchema,
148
- $defs: { ...defs, ...defSchema?.$defs },
149
- definitions: {
150
- ...defSchema.definitions,
151
- ...definitions,
152
- },
153
- };
154
- const pascalName = toPascalCase(defName);
155
- const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
156
- const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
157
- const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
198
+ const info = defInfoMap.get(defName);
199
+ const gid = info?.groupId || defName;
200
+ if (!groupById.has(gid))
201
+ groupById.set(gid, []);
202
+ groupById.get(gid).push(defName);
203
+ }
204
+ for (const [groupId, memberDefs] of groupById.entries()) {
205
+ const orderedDefs = orderGroupMembers(memberDefs, defInfoMap);
206
+ const members = orderedDefs.map((defName) => {
207
+ const defSchema = defs[defName];
208
+ const defSchemaWithDefs = {
209
+ ...defSchema,
210
+ $defs: { ...defs, ...defSchema?.$defs },
211
+ definitions: {
212
+ ...defSchema.definitions,
213
+ ...definitions,
214
+ },
215
+ };
216
+ const pascalName = toPascalCase(defName);
217
+ const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
218
+ const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
219
+ return { defName, schemaWithDefs: defSchemaWithDefs, schemaName, typeName };
220
+ });
221
+ const fileName = defInfoMap.get(memberDefs[0])?.fileName ?? `${memberDefs[0]}.schema.ts`;
158
222
  targets.push({
159
- defName,
160
- schemaWithDefs: defSchemaWithDefs,
161
- schemaName,
162
- typeName,
223
+ groupId,
163
224
  fileName,
225
+ members,
164
226
  usedRefs: new Set(),
165
227
  isRoot: false,
166
228
  });
@@ -168,17 +230,22 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
168
230
  if (options.splitDefs?.includeRoot ?? true) {
169
231
  const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
170
232
  targets.push({
171
- defName: null,
172
- schemaWithDefs: {
173
- ...rootSchema,
174
- definitions: {
175
- ...rootSchema.definitions,
176
- ...definitions,
177
- },
178
- },
179
- schemaName: rootName,
180
- typeName: rootTypeName,
233
+ groupId: "root",
181
234
  fileName: rootFile,
235
+ members: [
236
+ {
237
+ defName: null,
238
+ schemaWithDefs: {
239
+ ...rootSchema,
240
+ definitions: {
241
+ ...rootSchema.definitions,
242
+ ...definitions,
243
+ },
244
+ },
245
+ schemaName: rootName,
246
+ typeName: rootTypeName,
247
+ },
248
+ ],
182
249
  usedRefs: new Set(),
183
250
  isRoot: true,
184
251
  });
@@ -187,9 +254,13 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
187
254
  };
188
255
  const findRefDependencies = (schema, validDefNames) => {
189
256
  const deps = new Set();
257
+ const seen = new WeakSet();
190
258
  function traverse(obj) {
191
259
  if (obj === null || typeof obj !== "object")
192
260
  return;
261
+ if (seen.has(obj))
262
+ return;
263
+ seen.add(obj);
193
264
  if (Array.isArray(obj)) {
194
265
  obj.forEach(traverse);
195
266
  return;
@@ -209,6 +280,35 @@ const findRefDependencies = (schema, validDefNames) => {
209
280
  traverse(schema);
210
281
  return deps;
211
282
  };
283
+ const orderGroupMembers = (defs, defInfoMap) => {
284
+ const inGroup = new Set(defs);
285
+ const visited = new Set();
286
+ const temp = new Set();
287
+ const result = [];
288
+ const visit = (name) => {
289
+ if (visited.has(name))
290
+ return;
291
+ if (temp.has(name)) {
292
+ return;
293
+ }
294
+ temp.add(name);
295
+ const info = defInfoMap.get(name);
296
+ if (info) {
297
+ for (const dep of info.dependencies) {
298
+ if (inGroup.has(dep)) {
299
+ visit(dep);
300
+ }
301
+ }
302
+ }
303
+ temp.delete(name);
304
+ visited.add(name);
305
+ result.push(name);
306
+ };
307
+ for (const name of defs) {
308
+ visit(name);
309
+ }
310
+ return result;
311
+ };
212
312
  const detectCycles = (defInfoMap) => {
213
313
  const cycleNodes = new Set();
214
314
  const visited = new Set();
@@ -242,6 +342,52 @@ const detectCycles = (defInfoMap) => {
242
342
  }
243
343
  return cycleNodes;
244
344
  };
345
+ const buildSccGroups = (defInfoMap) => {
346
+ const indexMap = new Map();
347
+ const lowLink = new Map();
348
+ const onStack = new Set();
349
+ const stack = [];
350
+ let index = 0;
351
+ const groups = new Map();
352
+ const strongConnect = (node) => {
353
+ indexMap.set(node, index);
354
+ lowLink.set(node, index);
355
+ index += 1;
356
+ stack.push(node);
357
+ onStack.add(node);
358
+ const info = defInfoMap.get(node);
359
+ if (info) {
360
+ for (const dep of info.dependencies) {
361
+ if (!indexMap.has(dep)) {
362
+ strongConnect(dep);
363
+ lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(dep)));
364
+ }
365
+ else if (onStack.has(dep)) {
366
+ lowLink.set(node, Math.min(lowLink.get(node), indexMap.get(dep)));
367
+ }
368
+ }
369
+ }
370
+ if (lowLink.get(node) === indexMap.get(node)) {
371
+ const members = [];
372
+ let w;
373
+ do {
374
+ w = stack.pop();
375
+ if (w) {
376
+ onStack.delete(w);
377
+ members.push(w);
378
+ }
379
+ } while (w && w !== node);
380
+ const groupId = members.sort().join("__");
381
+ groups.set(groupId, members);
382
+ }
383
+ };
384
+ for (const name of defInfoMap.keys()) {
385
+ if (!indexMap.has(name)) {
386
+ strongConnect(name);
387
+ }
388
+ }
389
+ return groups;
390
+ };
245
391
  const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
246
392
  const allNestedTypes = [];
247
393
  for (const defName of defNames) {
@@ -317,6 +463,26 @@ const generateNestedTypesFile = (nestedTypes) => {
317
463
  " * They are extracted using TypeScript indexed access types.",
318
464
  " */",
319
465
  "",
466
+ "type Access<T, P extends readonly (string | number)[]> =",
467
+ " P extends []",
468
+ " ? NonNullable<T>",
469
+ " : P extends readonly [infer H, ...infer R]",
470
+ " ? H extends \"items\"",
471
+ " ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
472
+ " : H extends \"additionalProperties\"",
473
+ " ? Access<NonNullable<T> extends Record<string, infer V> ? V : unknown, Extract<R, (string | number)[]>>",
474
+ " : H extends number",
475
+ " ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
476
+ " : H extends string",
477
+ " ? Access<",
478
+ " H extends keyof NonNullable<T>",
479
+ " ? NonNullable<NonNullable<T>[H]>",
480
+ " : unknown,",
481
+ " Extract<R, (string | number)[]>",
482
+ " >",
483
+ " : unknown",
484
+ " : unknown;",
485
+ "",
320
486
  ];
321
487
  const byParent = new Map();
322
488
  for (const info of nestedTypes) {
@@ -336,16 +502,8 @@ const generateNestedTypesFile = (nestedTypes) => {
336
502
  }
337
503
  lines.push("");
338
504
  const buildAccessExpr = (parentType, propertyPath) => {
339
- let accessExpr = parentType;
340
- for (const prop of propertyPath) {
341
- const accessor = prop === "items"
342
- ? "[number]"
343
- : typeof prop === "number"
344
- ? `[${prop}]`
345
- : `[${JSON.stringify(prop)}]`;
346
- accessExpr = `NonNullable<${accessExpr}${accessor}>`;
347
- }
348
- return accessExpr;
505
+ const path = propertyPath.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop))).join(", ");
506
+ return `Access<${parentType}, [${path}]>`;
349
507
  };
350
508
  for (const [parentType, types] of [...byParent.entries()].sort()) {
351
509
  lines.push(`// From ${parentType}`);
@@ -24,10 +24,16 @@ export * from "./parsers/parseString.js";
24
24
  export * from "./utils/anyOrUnknown.js";
25
25
  export * from "./utils/buildRefRegistry.js";
26
26
  export * from "./utils/cycles.js";
27
+ export * from "./utils/esmEmitter.js";
28
+ export * from "./utils/extractInlineObject.js";
27
29
  export * from "./utils/half.js";
28
30
  export * from "./utils/jsdocs.js";
31
+ export * from "./utils/liftInlineObjects.js";
32
+ export * from "./utils/namingService.js";
29
33
  export * from "./utils/omit.js";
34
+ export * from "./utils/resolveRef.js";
30
35
  export * from "./utils/resolveUri.js";
36
+ export * from "./utils/schemaRepresentation.js";
31
37
  export * from "./utils/withMessage.js";
32
38
  export * from "./zodToJsonSchema.js";
33
39
  import { jsonSchemaToZod } from "./jsonSchemaToZod.js";
@@ -0,0 +1,17 @@
1
+ import { analyzeSchema } from "./core/analyzeSchema.js";
2
+ import { emitZod } from "./core/emitZod.js";
3
+ import { liftInlineObjects } from "./utils/liftInlineObjects.js";
4
+ export const jsonSchemaToZod = (schema, options = {}) => {
5
+ const liftOpts = options.liftInlineObjects ?? {};
6
+ const sourceSchema = liftOpts.enable !== false
7
+ ? liftInlineObjects(schema, {
8
+ enable: true,
9
+ nameForPath: liftOpts.nameForPath,
10
+ parentName: options.name,
11
+ dedup: liftOpts.dedup === true,
12
+ allowInDefs: liftOpts.allowInDefs,
13
+ }).schema
14
+ : schema;
15
+ const analysis = analyzeSchema(sourceSchema, options);
16
+ return emitZod(analysis);
17
+ };
@@ -0,0 +1,125 @@
1
+ import { parseSchema } from "./parseSchema.js";
2
+ import { half } from "../utils/half.js";
3
+ const originalIndexKey = "__originalIndex";
4
+ /**
5
+ * Check if a schema defines object properties (inline object shape) without any refs.
6
+ */
7
+ const isInlineObjectOnly = (schema) => {
8
+ if (typeof schema !== "object" || schema === null)
9
+ return false;
10
+ const obj = schema;
11
+ // Must have properties
12
+ if (!obj.properties || Object.keys(obj.properties).length === 0)
13
+ return false;
14
+ // Must NOT have $ref or $dynamicRef (can't use spread with refs)
15
+ if (obj.$ref || obj.$dynamicRef)
16
+ return false;
17
+ return true;
18
+ };
19
+ /**
20
+ * Parse just the shape entries from an object schema (without z.object wrapper).
21
+ * Returns array of "key: expression" strings for spreading.
22
+ */
23
+ const parseObjectShape = (schema, refs, pathPrefix) => {
24
+ const shapeEntries = [];
25
+ const shapeTypes = [];
26
+ for (const key of Object.keys(schema.properties)) {
27
+ const propSchema = schema.properties[key];
28
+ const parsedProp = parseSchema(propSchema, {
29
+ ...refs,
30
+ path: [...pathPrefix, "properties", key],
31
+ });
32
+ const hasDefault = typeof propSchema === "object" && propSchema.default !== undefined;
33
+ const required = Array.isArray(schema.required)
34
+ ? schema.required.includes(key)
35
+ : typeof propSchema === "object" && propSchema.required === true;
36
+ const optional = !hasDefault && !required;
37
+ const valueExpr = optional
38
+ ? `${parsedProp.expression}.optional()`
39
+ : parsedProp.expression;
40
+ const valueType = optional
41
+ ? `z.ZodOptional<${parsedProp.type}>`
42
+ : parsedProp.type;
43
+ shapeEntries.push(`${JSON.stringify(key)}: ${valueExpr}`);
44
+ shapeTypes.push(`${JSON.stringify(key)}: ${valueType}`);
45
+ }
46
+ return { shapeEntries, shapeTypes };
47
+ };
48
+ /**
49
+ * Check if all allOf members can be combined using spread syntax.
50
+ * Only works when ALL members are inline objects (no $refs).
51
+ * Returns the merged object if possible, undefined otherwise.
52
+ */
53
+ const trySpreadPattern = (allOfMembers, refs) => {
54
+ const shapeEntries = [];
55
+ const shapeTypes = [];
56
+ for (let i = 0; i < allOfMembers.length; i++) {
57
+ const member = allOfMembers[i];
58
+ const idx = member[originalIndexKey] ?? i;
59
+ // Only handle pure inline objects - no refs allowed
60
+ if (!isInlineObjectOnly(member)) {
61
+ return undefined;
62
+ }
63
+ // Extract shape entries from inline object
64
+ const { shapeEntries: entries, shapeTypes: types } = parseObjectShape(member, refs, [...refs.path, "allOf", idx]);
65
+ shapeEntries.push(...entries);
66
+ shapeTypes.push(...types);
67
+ }
68
+ if (shapeEntries.length === 0)
69
+ return undefined;
70
+ return {
71
+ expression: `z.object({ ${shapeEntries.join(", ")} })`,
72
+ type: `z.ZodObject<{ ${shapeTypes.join(", ")} }>`,
73
+ };
74
+ };
75
+ const ensureOriginalIndex = (arr) => {
76
+ const newArr = [];
77
+ for (let i = 0; i < arr.length; i++) {
78
+ const item = arr[i];
79
+ if (typeof item === "boolean") {
80
+ newArr.push(item ? { [originalIndexKey]: i } : { [originalIndexKey]: i, not: {} });
81
+ }
82
+ else if (typeof item === "object" &&
83
+ item !== null &&
84
+ originalIndexKey in item) {
85
+ return arr;
86
+ }
87
+ else {
88
+ newArr.push({ ...item, [originalIndexKey]: i });
89
+ }
90
+ }
91
+ return newArr;
92
+ };
93
+ export function parseAllOf(schema, refs) {
94
+ if (schema.allOf.length === 0) {
95
+ return { expression: "z.never()", type: "z.ZodNever" };
96
+ }
97
+ else if (schema.allOf.length === 1) {
98
+ const item = schema.allOf[0];
99
+ return parseSchema(item, {
100
+ ...refs,
101
+ path: [
102
+ ...refs.path,
103
+ "allOf",
104
+ item[originalIndexKey] ?? 0,
105
+ ],
106
+ });
107
+ }
108
+ else {
109
+ // Try spread pattern first (more efficient than intersection)
110
+ // This works when all members are either $refs to object schemas or inline objects
111
+ const indexed = ensureOriginalIndex(schema.allOf);
112
+ const spreadResult = trySpreadPattern(indexed, refs);
113
+ if (spreadResult) {
114
+ return spreadResult;
115
+ }
116
+ // Fallback to intersection-based approach
117
+ const [left, right] = half(indexed);
118
+ const leftResult = parseAllOf({ allOf: left }, refs);
119
+ const rightResult = parseAllOf({ allOf: right }, refs);
120
+ return {
121
+ expression: `z.intersection(${leftResult.expression}, ${rightResult.expression})`,
122
+ type: `z.ZodIntersection<${leftResult.type}, ${rightResult.type}>`,
123
+ };
124
+ }
125
+ }
@@ -0,0 +1,28 @@
1
+ import { parseSchema } from "./parseSchema.js";
2
+ import { anyOrUnknown } from "../utils/anyOrUnknown.js";
3
+ import { extractInlineObject } from "../utils/extractInlineObject.js";
4
+ export const parseAnyOf = (schema, refs) => {
5
+ if (!schema.anyOf.length) {
6
+ return anyOrUnknown(refs);
7
+ }
8
+ if (schema.anyOf.length === 1) {
9
+ return parseSchema(schema.anyOf[0], {
10
+ ...refs,
11
+ path: [...refs.path, "anyOf", 0],
12
+ });
13
+ }
14
+ // Rule 1: Extract inline objects to top-level declarations
15
+ const members = schema.anyOf.map((memberSchema, i) => {
16
+ const extracted = extractInlineObject(memberSchema, refs, [...refs.path, "anyOf", i]);
17
+ if (extracted) {
18
+ return { expression: extracted, type: `typeof ${extracted}` };
19
+ }
20
+ return parseSchema(memberSchema, { ...refs, path: [...refs.path, "anyOf", i] });
21
+ });
22
+ const expressions = members.map(m => m.expression).join(", ");
23
+ const types = members.map(m => m.type).join(", ");
24
+ const expression = `z.union([${expressions}])`;
25
+ // Use readonly tuple for union type annotations (required for recursive type inference)
26
+ const type = `z.ZodUnion<readonly [${types}]>`;
27
+ return { expression, type };
28
+ };