@gabrielbryk/json-schema-to-zod 2.12.1 → 2.14.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/.github/RELEASE_SETUP.md +120 -0
- package/.github/TOOLING_GUIDE.md +169 -0
- package/.github/dependabot.yml +52 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/release.yml +12 -4
- package/.github/workflows/security.yml +40 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.lintstagedrc.json +3 -0
- package/.prettierrc +20 -0
- package/AGENTS.md +7 -0
- package/CHANGELOG.md +13 -4
- package/README.md +24 -9
- package/commitlint.config.js +24 -0
- package/createIndex.ts +4 -4
- package/dist/cli.js +3 -4
- package/dist/core/analyzeSchema.js +56 -11
- package/dist/core/emitZod.js +43 -12
- package/dist/generators/generateBundle.js +67 -92
- package/dist/index.js +1 -0
- package/dist/parsers/parseAllOf.js +11 -12
- package/dist/parsers/parseAnyOf.js +2 -2
- package/dist/parsers/parseArray.js +38 -12
- package/dist/parsers/parseMultipleType.js +2 -2
- package/dist/parsers/parseNumber.js +44 -102
- package/dist/parsers/parseObject.js +136 -443
- package/dist/parsers/parseOneOf.js +57 -110
- package/dist/parsers/parseSchema.js +176 -71
- package/dist/parsers/parseSimpleDiscriminatedOneOf.js +2 -2
- package/dist/parsers/parseString.js +113 -253
- package/dist/types/Types.d.ts +37 -1
- package/dist/types/core/analyzeSchema.d.ts +4 -0
- package/dist/types/generators/generateBundle.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/utils/schemaNaming.d.ts +6 -0
- package/dist/utils/cliTools.js +1 -2
- package/dist/utils/esmEmitter.js +6 -2
- package/dist/utils/extractInlineObject.js +1 -3
- package/dist/utils/jsdocs.js +1 -4
- package/dist/utils/liftInlineObjects.js +76 -15
- package/dist/utils/resolveRef.js +35 -10
- package/dist/utils/schemaNaming.js +31 -0
- package/dist/utils/schemaRepresentation.js +35 -66
- package/dist/zodToJsonSchema.js +1 -2
- package/docs/IMPROVEMENT-PLAN.md +30 -12
- package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +70 -25
- package/docs/proposals/allof-required-merging.md +10 -4
- package/docs/proposals/bundle-refactor.md +10 -4
- package/docs/proposals/discriminated-union-with-default.md +18 -14
- package/docs/proposals/inline-object-lifting.md +15 -5
- package/docs/proposals/ref-anchor-support.md +11 -0
- package/output.txt +67 -0
- package/package.json +18 -5
- package/scripts/generateWorkflowSchema.ts +5 -14
- package/scripts/regenerate_bundle.ts +25 -0
- package/tsc_output.txt +542 -0
- package/tsc_output_2.txt +489 -0
package/dist/cli.js
CHANGED
|
@@ -7,8 +7,7 @@ const params = {
|
|
|
7
7
|
input: {
|
|
8
8
|
shorthand: "i",
|
|
9
9
|
value: "string",
|
|
10
|
-
required: process.stdin.isTTY &&
|
|
11
|
-
"input is required when no JSON or file path is piped",
|
|
10
|
+
required: process.stdin.isTTY && "input is required when no JSON or file path is piped",
|
|
12
11
|
description: "JSON or a source file path. Required if no data is piped.",
|
|
13
12
|
},
|
|
14
13
|
output: {
|
|
@@ -29,11 +28,11 @@ const params = {
|
|
|
29
28
|
type: {
|
|
30
29
|
shorthand: "t",
|
|
31
30
|
value: "string",
|
|
32
|
-
description: "The name of the (optional) inferred type export."
|
|
31
|
+
description: "The name of the (optional) inferred type export.",
|
|
33
32
|
},
|
|
34
33
|
noImport: {
|
|
35
34
|
shorthand: "ni",
|
|
36
|
-
description: "Removes the `import { z } from 'zod';` or equivalent from the output."
|
|
35
|
+
description: "Removes the `import { z } from 'zod';` or equivalent from the output.",
|
|
37
36
|
},
|
|
38
37
|
withJsdocs: {
|
|
39
38
|
shorthand: "wj",
|
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
import { parseSchema } from "../parsers/parseSchema.js";
|
|
2
2
|
import { detectCycles, computeScc } from "../utils/cycles.js";
|
|
3
3
|
import { buildRefRegistry } from "../utils/buildRefRegistry.js";
|
|
4
|
+
import { resolveSchemaName } from "../utils/schemaNaming.js";
|
|
4
5
|
export const analyzeSchema = (schema, options = {}) => {
|
|
5
|
-
const { name, type, ...rest } = options;
|
|
6
|
+
const { name, type, naming, ...rest } = options;
|
|
6
7
|
if (type && !name) {
|
|
7
8
|
throw new Error("Option `type` requires `name` to be set");
|
|
8
9
|
}
|
|
10
|
+
const nameContext = { isRoot: true, isLifted: false };
|
|
11
|
+
const rootBaseName = name;
|
|
12
|
+
let resolvedName = name;
|
|
13
|
+
const usedNames = new Set();
|
|
14
|
+
const usedBaseNames = new Set();
|
|
15
|
+
if (name && naming) {
|
|
16
|
+
resolvedName = resolveSchemaName(name, naming, nameContext, usedNames);
|
|
17
|
+
usedBaseNames.add(name);
|
|
18
|
+
}
|
|
19
|
+
if (resolvedName) {
|
|
20
|
+
usedNames.add(resolvedName);
|
|
21
|
+
}
|
|
9
22
|
const normalized = {
|
|
10
|
-
name,
|
|
23
|
+
name: resolvedName,
|
|
11
24
|
type,
|
|
25
|
+
naming,
|
|
12
26
|
module: "esm",
|
|
13
27
|
...rest,
|
|
14
28
|
exportRefs: rest.exportRefs ?? true,
|
|
15
29
|
withMeta: rest.withMeta ?? true,
|
|
16
30
|
};
|
|
17
31
|
const refNameByPointer = new Map();
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
const refBaseNameByPointer = new Map();
|
|
33
|
+
const baseNameBySchema = new Map();
|
|
34
|
+
if (naming && resolvedName && rootBaseName) {
|
|
35
|
+
baseNameBySchema.set(resolvedName, rootBaseName);
|
|
21
36
|
}
|
|
22
37
|
const declarations = new Map();
|
|
23
38
|
const dependencies = new Map();
|
|
24
|
-
|
|
39
|
+
// Use provided registry or build a new one for this schema
|
|
40
|
+
let refRegistry = rest.refRegistry;
|
|
41
|
+
let rootBaseUri = rest.rootBaseUri ?? "root:///";
|
|
42
|
+
if (!refRegistry) {
|
|
43
|
+
const built = buildRefRegistry(schema, rootBaseUri);
|
|
44
|
+
refRegistry = built.registry;
|
|
45
|
+
rootBaseUri = built.rootBaseUri;
|
|
46
|
+
}
|
|
25
47
|
const pass1 = {
|
|
26
48
|
name,
|
|
27
49
|
path: [],
|
|
@@ -30,28 +52,51 @@ export const analyzeSchema = (schema, options = {}) => {
|
|
|
30
52
|
dependencies,
|
|
31
53
|
inProgress: new Set(),
|
|
32
54
|
refNameByPointer,
|
|
55
|
+
refBaseNameByPointer,
|
|
56
|
+
baseNameBySchema,
|
|
33
57
|
usedNames,
|
|
58
|
+
usedBaseNames,
|
|
34
59
|
root: schema,
|
|
35
|
-
currentSchemaName:
|
|
60
|
+
currentSchemaName: resolvedName,
|
|
36
61
|
refRegistry,
|
|
37
62
|
rootBaseUri,
|
|
38
63
|
...rest,
|
|
39
64
|
withMeta: normalized.withMeta,
|
|
65
|
+
naming,
|
|
40
66
|
};
|
|
41
67
|
parseSchema(schema, pass1);
|
|
42
68
|
const names = Array.from(declarations.keys());
|
|
43
69
|
const cycleRefNames = detectCycles(names, dependencies);
|
|
44
70
|
const { componentByName } = computeScc(names, dependencies);
|
|
71
|
+
// Pass 2: Re-parse with cycle information if cycles were detected.
|
|
72
|
+
// This allows parseRef to correctly identify cyclic references and wrap them in z.lazy().
|
|
73
|
+
if (cycleRefNames.size > 0) {
|
|
74
|
+
declarations.clear();
|
|
75
|
+
pass1.seen.clear();
|
|
76
|
+
pass1.inProgress.clear();
|
|
77
|
+
// We reuse refNameByPointer to ensure stable naming across passes
|
|
78
|
+
const pass2 = {
|
|
79
|
+
...pass1,
|
|
80
|
+
declarations,
|
|
81
|
+
cycleRefNames,
|
|
82
|
+
cycleComponentByName: componentByName,
|
|
83
|
+
};
|
|
84
|
+
parseSchema(schema, pass2);
|
|
85
|
+
}
|
|
45
86
|
return {
|
|
46
87
|
schema,
|
|
47
88
|
options: normalized,
|
|
48
|
-
refNameByPointer,
|
|
49
|
-
usedNames,
|
|
50
89
|
declarations,
|
|
90
|
+
definitions: {}, // Legacy support
|
|
51
91
|
dependencies,
|
|
92
|
+
refNameByPointer,
|
|
93
|
+
refBaseNameByPointer,
|
|
94
|
+
baseNameBySchema,
|
|
95
|
+
rootBaseName,
|
|
96
|
+
usedNames,
|
|
52
97
|
cycleRefNames,
|
|
53
98
|
cycleComponentByName: componentByName,
|
|
54
|
-
|
|
55
|
-
|
|
99
|
+
rootBaseUri: pass1.rootBaseUri,
|
|
100
|
+
refRegistry: pass1.refRegistry,
|
|
56
101
|
};
|
|
57
102
|
};
|
package/dist/core/emitZod.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parseSchema } from "../parsers/parseSchema.js";
|
|
|
2
2
|
import { expandJsdocs } from "../utils/jsdocs.js";
|
|
3
3
|
import { inferTypeFromExpression } from "../utils/schemaRepresentation.js";
|
|
4
4
|
import { EsmEmitter } from "../utils/esmEmitter.js";
|
|
5
|
+
import { resolveTypeName } from "../utils/schemaNaming.js";
|
|
5
6
|
/**
|
|
6
7
|
* Split a z.object({...}).method1().method2() expression into base and method chain.
|
|
7
8
|
* This is needed for Rule 2: Don't chain methods on recursive types.
|
|
@@ -148,8 +149,8 @@ const orderDeclarations = (entries, dependencies) => {
|
|
|
148
149
|
return ordered.map((name) => [name, repByName.get(name)]);
|
|
149
150
|
};
|
|
150
151
|
export const emitZod = (analysis) => {
|
|
151
|
-
const { schema, options, refNameByPointer, cycleRefNames, cycleComponentByName, } = analysis;
|
|
152
|
-
const { name, type, noImport, exportRefs, typeExports, withMeta, ...rest } = options;
|
|
152
|
+
const { schema, options, refNameByPointer, cycleRefNames, cycleComponentByName, baseNameBySchema, rootBaseName, } = analysis;
|
|
153
|
+
const { name, type, naming, noImport, exportRefs, typeExports, withMeta, ...rest } = options;
|
|
153
154
|
const declarations = new Map();
|
|
154
155
|
const dependencies = new Map();
|
|
155
156
|
// Fresh name registry for the emission pass.
|
|
@@ -175,6 +176,7 @@ export const emitZod = (analysis) => {
|
|
|
175
176
|
rootBaseUri: analysis.rootBaseUri,
|
|
176
177
|
...rest,
|
|
177
178
|
withMeta,
|
|
179
|
+
naming,
|
|
178
180
|
});
|
|
179
181
|
const jsdocs = rest.withJsdocs && typeof schema === "object" && schema !== null && "description" in schema
|
|
180
182
|
? expandJsdocs(typeof schema.description === "string"
|
|
@@ -182,6 +184,13 @@ export const emitZod = (analysis) => {
|
|
|
182
184
|
: "")
|
|
183
185
|
: "";
|
|
184
186
|
const emitter = new EsmEmitter();
|
|
187
|
+
const usedTypeNames = new Set();
|
|
188
|
+
const resolveDeclarationTypeName = (schemaName) => {
|
|
189
|
+
if (!naming)
|
|
190
|
+
return schemaName;
|
|
191
|
+
const baseName = baseNameBySchema.get(schemaName) ?? schemaName;
|
|
192
|
+
return resolveTypeName(baseName, naming, { isRoot: false, isLifted: true }, usedTypeNames);
|
|
193
|
+
};
|
|
185
194
|
if (!noImport) {
|
|
186
195
|
emitter.addNamedImport("z", "zod");
|
|
187
196
|
}
|
|
@@ -191,21 +200,27 @@ export const emitZod = (analysis) => {
|
|
|
191
200
|
if (typeof expression !== "string") {
|
|
192
201
|
throw new Error(`Expected declaration expression for ${refName}`);
|
|
193
202
|
}
|
|
194
|
-
const hintedType = typeof rep === "object" &&
|
|
203
|
+
const hintedType = typeof rep === "object" &&
|
|
204
|
+
rep &&
|
|
205
|
+
"type" in rep &&
|
|
206
|
+
typeof rep.type === "string"
|
|
195
207
|
? rep.type
|
|
196
208
|
: undefined;
|
|
209
|
+
const effectiveHint = hintedType === "z.ZodTypeAny" ? undefined : hintedType;
|
|
197
210
|
const hasLazy = expression.includes("z.lazy(");
|
|
198
211
|
const hasGetter = expression.includes("get ");
|
|
199
212
|
// Check if this schema references any cycle members (recursive schemas)
|
|
200
213
|
// This can cause TS7056 when TypeScript tries to serialize the expanded type
|
|
201
|
-
const referencesRecursiveSchema = Array.from(cycleRefNames).some(cycleName => new RegExp(`\\b${cycleName}\\b`).test(expression));
|
|
214
|
+
const referencesRecursiveSchema = Array.from(cycleRefNames).some((cycleName) => new RegExp(`\\b${cycleName}\\b`).test(expression));
|
|
202
215
|
// Per Zod v4 docs: type annotations should be on GETTERS for recursive types, not on const declarations.
|
|
203
216
|
// TypeScript can infer the type of const declarations.
|
|
204
217
|
// Exceptions that need explicit type annotation:
|
|
205
218
|
// 1. z.lazy() without getters
|
|
206
219
|
// 2. Any schema that references recursive schemas (to prevent TS7056)
|
|
207
220
|
const needsTypeAnnotation = (hasLazy && !hasGetter) || referencesRecursiveSchema;
|
|
208
|
-
const storedType = needsTypeAnnotation
|
|
221
|
+
const storedType = needsTypeAnnotation
|
|
222
|
+
? (effectiveHint ?? inferTypeFromExpression(expression))
|
|
223
|
+
: undefined;
|
|
209
224
|
// Rule 2 from Zod v4: Don't chain methods on recursive types
|
|
210
225
|
// If the schema has getters (recursive), we need to split it:
|
|
211
226
|
// 1. Emit base schema as _RefName
|
|
@@ -226,13 +241,18 @@ export const emitZod = (analysis) => {
|
|
|
226
241
|
name: refName,
|
|
227
242
|
expression: `${baseName}${methodChain}`,
|
|
228
243
|
exported: exportRefs,
|
|
244
|
+
typeAnnotation: storedType !== "z.ZodTypeAny" ? storedType : undefined,
|
|
229
245
|
});
|
|
230
246
|
// Export type for this declaration if typeExports is enabled
|
|
231
247
|
if (typeExports && exportRefs) {
|
|
248
|
+
const typeName = resolveDeclarationTypeName(refName);
|
|
232
249
|
emitter.addTypeExport({
|
|
233
|
-
name: refName,
|
|
250
|
+
name: typeName ?? refName,
|
|
234
251
|
type: `z.infer<typeof ${refName}>`,
|
|
235
252
|
});
|
|
253
|
+
if (typeName) {
|
|
254
|
+
usedTypeNames.add(typeName);
|
|
255
|
+
}
|
|
236
256
|
}
|
|
237
257
|
continue;
|
|
238
258
|
}
|
|
@@ -245,10 +265,14 @@ export const emitZod = (analysis) => {
|
|
|
245
265
|
});
|
|
246
266
|
// Export type for this declaration if typeExports is enabled
|
|
247
267
|
if (typeExports && exportRefs) {
|
|
268
|
+
const typeName = resolveDeclarationTypeName(refName);
|
|
248
269
|
emitter.addTypeExport({
|
|
249
|
-
name: refName,
|
|
270
|
+
name: typeName ?? refName,
|
|
250
271
|
type: `z.infer<typeof ${refName}>`,
|
|
251
272
|
});
|
|
273
|
+
if (typeName) {
|
|
274
|
+
usedTypeNames.add(typeName);
|
|
275
|
+
}
|
|
252
276
|
}
|
|
253
277
|
}
|
|
254
278
|
}
|
|
@@ -268,11 +292,18 @@ export const emitZod = (analysis) => {
|
|
|
268
292
|
}
|
|
269
293
|
// Export type for root schema if type option is set, or if typeExports is enabled
|
|
270
294
|
if (name && (type || typeExports)) {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
295
|
+
const rootTypeName = typeof type === "string"
|
|
296
|
+
? type
|
|
297
|
+
: naming && rootBaseName
|
|
298
|
+
? resolveTypeName(rootBaseName, naming, { isRoot: true, isLifted: false }, usedTypeNames)
|
|
299
|
+
: `${name[0].toUpperCase()}${name.substring(1)}`;
|
|
300
|
+
if (rootTypeName) {
|
|
301
|
+
emitter.addTypeExport({
|
|
302
|
+
name: rootTypeName,
|
|
303
|
+
type: `z.infer<typeof ${name}>`,
|
|
304
|
+
});
|
|
305
|
+
usedTypeNames.add(rootTypeName);
|
|
306
|
+
}
|
|
276
307
|
}
|
|
277
308
|
return emitter.render();
|
|
278
309
|
};
|
|
@@ -11,29 +11,31 @@ export const generateSchemaBundle = (schema, options = {}) => {
|
|
|
11
11
|
? liftInlineObjects(schema, {
|
|
12
12
|
enable: true,
|
|
13
13
|
nameForPath: liftOpts.nameForPath,
|
|
14
|
-
parentName: options.splitDefs?.rootTypeName ??
|
|
14
|
+
parentName: options.splitDefs?.rootTypeName ??
|
|
15
|
+
options.splitDefs?.rootName ??
|
|
16
|
+
schema.title,
|
|
15
17
|
dedup: liftOpts.dedup === true,
|
|
16
18
|
allowInDefs: liftOpts.allowInDefs,
|
|
17
19
|
}).schema
|
|
18
20
|
: schema;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const allDefs = {
|
|
22
|
+
...liftedSchema.definitions,
|
|
23
|
+
...liftedSchema.$defs,
|
|
24
|
+
};
|
|
25
|
+
const defNames = Object.keys(allDefs);
|
|
26
|
+
const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, allDefs, options);
|
|
23
27
|
const files = [];
|
|
24
|
-
const targets = planBundleTargets(liftedSchema,
|
|
28
|
+
const targets = planBundleTargets(liftedSchema, allDefs, {}, defNames, options, rootName, defInfoMap, rootTypeName);
|
|
25
29
|
for (const target of targets) {
|
|
26
|
-
const usedRefs =
|
|
30
|
+
const usedRefs = new Set();
|
|
27
31
|
const zodParts = [];
|
|
28
32
|
for (const member of target.members) {
|
|
29
33
|
const analysis = analyzeSchema(member.schemaWithDefs, {
|
|
30
34
|
...options,
|
|
31
35
|
name: member.schemaName,
|
|
32
36
|
type: member.typeName,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
...(member.schemaWithDefs.definitions || {}),
|
|
36
|
-
}, options, target.groupId),
|
|
37
|
+
documentRoot: liftedSchema,
|
|
38
|
+
parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, allDefs, options, target.groupId),
|
|
37
39
|
});
|
|
38
40
|
const zodSchema = emitZod(analysis);
|
|
39
41
|
zodParts.push(zodSchema);
|
|
@@ -44,7 +46,7 @@ export const generateSchemaBundle = (schema, options = {}) => {
|
|
|
44
46
|
// Nested types extraction (optional)
|
|
45
47
|
const nestedTypesEnabled = options.nestedTypes?.enable;
|
|
46
48
|
if (nestedTypesEnabled) {
|
|
47
|
-
const nestedTypes = collectNestedTypes(liftedSchema,
|
|
49
|
+
const nestedTypes = collectNestedTypes(liftedSchema, allDefs, defNames, rootTypeName ?? rootName);
|
|
48
50
|
if (nestedTypes.length > 0) {
|
|
49
51
|
const nestedFileName = options.nestedTypes?.fileName ?? "nested-types.ts";
|
|
50
52
|
const nestedContent = generateNestedTypesFile(nestedTypes);
|
|
@@ -89,19 +91,28 @@ const buildBundleContext = (defNames, defs, options) => {
|
|
|
89
91
|
if (info)
|
|
90
92
|
info.hasCycle = true;
|
|
91
93
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
/*
|
|
95
|
+
const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
|
|
96
|
+
// NOTE: SCC grouping is currently disabled to ensure 1-to-1 mapping of $defs to files,
|
|
97
|
+
// which is expected by the test suite and preferred for clarity.
|
|
98
|
+
if (!useLazyCrossRefs) {
|
|
99
|
+
const groups = buildSccGroups(defInfoMap);
|
|
100
|
+
for (const [groupId, members] of groups) {
|
|
101
|
+
if (members.length > 1) {
|
|
102
|
+
for (const defName of members) {
|
|
95
103
|
const info = defInfoMap.get(defName);
|
|
96
|
-
if (info)
|
|
97
|
-
|
|
104
|
+
if (info) info.groupId = groupId;
|
|
105
|
+
}
|
|
98
106
|
}
|
|
107
|
+
}
|
|
99
108
|
}
|
|
109
|
+
*/
|
|
100
110
|
const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
|
|
101
111
|
const rootTypeName = typeof options.type === "string"
|
|
102
112
|
? options.type
|
|
103
|
-
: options.splitDefs?.rootTypeName ??
|
|
104
|
-
|
|
113
|
+
: (options.splitDefs?.rootTypeName ??
|
|
114
|
+
(typeof options.type === "boolean" && options.type ? rootName : undefined));
|
|
115
|
+
return { defInfoMap, rootName, rootTypeName };
|
|
105
116
|
};
|
|
106
117
|
const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
|
|
107
118
|
const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
|
|
@@ -111,10 +122,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
111
122
|
const match = refPath.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
|
|
112
123
|
if (match) {
|
|
113
124
|
const refName = match[1];
|
|
114
|
-
//
|
|
115
|
-
if (refName.includes("/")) {
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
125
|
+
// First check if it's exactly a top-level definition
|
|
118
126
|
const refInfo = defInfoMap.get(refName);
|
|
119
127
|
if (refInfo) {
|
|
120
128
|
// Track imports when referencing other defs
|
|
@@ -131,25 +139,26 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
131
139
|
});
|
|
132
140
|
if (resolved)
|
|
133
141
|
return resolved;
|
|
142
|
+
// Self-recursion ALWAYS needs z.lazy if not using getters
|
|
143
|
+
if (refName === currentDefName) {
|
|
144
|
+
return `z.lazy(() => ${refInfo.schemaName})`;
|
|
145
|
+
}
|
|
134
146
|
if (isCycle && useLazyCrossRefs) {
|
|
135
|
-
|
|
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})`;
|
|
147
|
+
return `z.lazy(() => ${refInfo.schemaName})`;
|
|
143
148
|
}
|
|
144
149
|
return refInfo.schemaName;
|
|
145
150
|
}
|
|
146
|
-
// If
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
// If it's NOT exactly a top-level definition, it could be:
|
|
152
|
+
// 1. A path into a top-level definition (e.g. #/$defs/alpha/properties/foo)
|
|
153
|
+
// 2. A local/inline definition NOT in allDefs
|
|
154
|
+
// 3. A reference to allDefs that we missed? (shouldn't happen)
|
|
155
|
+
// We return undefined to let the standard parser resolve it.
|
|
156
|
+
return undefined;
|
|
151
157
|
}
|
|
152
|
-
const unknown = options.refResolution?.onUnknownRef?.({
|
|
158
|
+
const unknown = options.refResolution?.onUnknownRef?.({
|
|
159
|
+
ref: refPath,
|
|
160
|
+
currentDef: currentDefName,
|
|
161
|
+
});
|
|
153
162
|
if (unknown)
|
|
154
163
|
return unknown;
|
|
155
164
|
return options.useUnknown ? "z.unknown()" : "z.any()";
|
|
@@ -160,7 +169,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
160
169
|
const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap) => {
|
|
161
170
|
const groupFileById = new Map();
|
|
162
171
|
for (const info of defInfoMap.values()) {
|
|
163
|
-
if (!groupFileById.has(info.groupId)) {
|
|
172
|
+
if (info.groupId && !groupFileById.has(info.groupId)) {
|
|
164
173
|
groupFileById.set(info.groupId, info.fileName.replace(/\.ts$/, ".js"));
|
|
165
174
|
}
|
|
166
175
|
}
|
|
@@ -168,7 +177,8 @@ const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap) => {
|
|
|
168
177
|
for (const refName of [...usedRefs].sort()) {
|
|
169
178
|
const refInfo = defInfoMap.get(refName);
|
|
170
179
|
if (refInfo) {
|
|
171
|
-
const groupFile = groupFileById.get(refInfo.groupId)
|
|
180
|
+
const groupFile = (refInfo.groupId ? groupFileById.get(refInfo.groupId) : null) ??
|
|
181
|
+
refInfo.fileName.replace(/\.ts$/, ".js");
|
|
172
182
|
const path = `./${groupFile}`;
|
|
173
183
|
const set = importsByFile.get(path) ?? new Set();
|
|
174
184
|
set.add(refInfo.schemaName);
|
|
@@ -187,7 +197,7 @@ const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap) => {
|
|
|
187
197
|
})
|
|
188
198
|
.join("\n");
|
|
189
199
|
const withImports = imports.length
|
|
190
|
-
? body.replace(
|
|
200
|
+
? body.replace(/import \{ z \} from "zod";?/, `import { z } from "zod";\n${imports.join("\n")}`)
|
|
191
201
|
: body;
|
|
192
202
|
return withImports;
|
|
193
203
|
};
|
|
@@ -207,7 +217,10 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
|
|
|
207
217
|
const defSchema = defs[defName];
|
|
208
218
|
const defSchemaWithDefs = {
|
|
209
219
|
...defSchema,
|
|
210
|
-
$defs: {
|
|
220
|
+
$defs: {
|
|
221
|
+
...defs,
|
|
222
|
+
...defSchema?.$defs,
|
|
223
|
+
},
|
|
211
224
|
definitions: {
|
|
212
225
|
...defSchema.definitions,
|
|
213
226
|
...definitions,
|
|
@@ -342,52 +355,6 @@ const detectCycles = (defInfoMap) => {
|
|
|
342
355
|
}
|
|
343
356
|
return cycleNodes;
|
|
344
357
|
};
|
|
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
|
-
};
|
|
391
358
|
const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
|
|
392
359
|
const allNestedTypes = [];
|
|
393
360
|
for (const defName of defNames) {
|
|
@@ -400,7 +367,10 @@ const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
|
|
|
400
367
|
allNestedTypes.push(nested);
|
|
401
368
|
}
|
|
402
369
|
}
|
|
403
|
-
const workflowNestedTypes = findNestedTypesInSchema({
|
|
370
|
+
const workflowNestedTypes = findNestedTypesInSchema({
|
|
371
|
+
properties: rootSchema.properties,
|
|
372
|
+
required: rootSchema.required,
|
|
373
|
+
}, rootTypeName, defNames);
|
|
404
374
|
for (const nested of workflowNestedTypes) {
|
|
405
375
|
nested.file = "workflow";
|
|
406
376
|
nested.parentType = rootTypeName;
|
|
@@ -450,7 +420,10 @@ const findNestedTypesInSchema = (schema, parentTypeName, defNames, currentPath =
|
|
|
450
420
|
nestedTypes.push(...findNestedTypesInSchema(record.items, parentTypeName, defNames, [...currentPath, "items"]));
|
|
451
421
|
}
|
|
452
422
|
if (record.additionalProperties && typeof record.additionalProperties === "object") {
|
|
453
|
-
nestedTypes.push(...findNestedTypesInSchema(record.additionalProperties, parentTypeName, defNames, [
|
|
423
|
+
nestedTypes.push(...findNestedTypesInSchema(record.additionalProperties, parentTypeName, defNames, [
|
|
424
|
+
...currentPath,
|
|
425
|
+
"additionalProperties",
|
|
426
|
+
]));
|
|
454
427
|
}
|
|
455
428
|
return nestedTypes;
|
|
456
429
|
};
|
|
@@ -467,9 +440,9 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
467
440
|
" P extends []",
|
|
468
441
|
" ? NonNullable<T>",
|
|
469
442
|
" : P extends readonly [infer H, ...infer R]",
|
|
470
|
-
|
|
443
|
+
' ? H extends "items"',
|
|
471
444
|
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
472
|
-
|
|
445
|
+
' : H extends "additionalProperties"',
|
|
473
446
|
" ? Access<NonNullable<T> extends Record<string, infer V> ? V : unknown, Extract<R, (string | number)[]>>",
|
|
474
447
|
" : H extends number",
|
|
475
448
|
" ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
|
|
@@ -502,7 +475,9 @@ const generateNestedTypesFile = (nestedTypes) => {
|
|
|
502
475
|
}
|
|
503
476
|
lines.push("");
|
|
504
477
|
const buildAccessExpr = (parentType, propertyPath) => {
|
|
505
|
-
const path = propertyPath
|
|
478
|
+
const path = propertyPath
|
|
479
|
+
.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop)))
|
|
480
|
+
.join(", ");
|
|
506
481
|
return `Access<${parentType}, [${path}]>`;
|
|
507
482
|
};
|
|
508
483
|
for (const [parentType, types] of [...byParent.entries()].sort()) {
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./utils/namingService.js";
|
|
|
33
33
|
export * from "./utils/omit.js";
|
|
34
34
|
export * from "./utils/resolveRef.js";
|
|
35
35
|
export * from "./utils/resolveUri.js";
|
|
36
|
+
export * from "./utils/schemaNaming.js";
|
|
36
37
|
export * from "./utils/schemaRepresentation.js";
|
|
37
38
|
export * from "./utils/withMessage.js";
|
|
38
39
|
export * from "./zodToJsonSchema.js";
|
|
@@ -34,12 +34,8 @@ const parseObjectShape = (schema, refs, pathPrefix) => {
|
|
|
34
34
|
? schema.required.includes(key)
|
|
35
35
|
: typeof propSchema === "object" && propSchema.required === true;
|
|
36
36
|
const optional = !hasDefault && !required;
|
|
37
|
-
const valueExpr = optional
|
|
38
|
-
|
|
39
|
-
: parsedProp.expression;
|
|
40
|
-
const valueType = optional
|
|
41
|
-
? `z.ZodOptional<${parsedProp.type}>`
|
|
42
|
-
: parsedProp.type;
|
|
37
|
+
const valueExpr = optional ? `${parsedProp.expression}.exactOptional()` : parsedProp.expression;
|
|
38
|
+
const valueType = optional ? `z.ZodExactOptional<${parsedProp.type}>` : parsedProp.type;
|
|
43
39
|
shapeEntries.push(`${JSON.stringify(key)}: ${valueExpr}`);
|
|
44
40
|
shapeTypes.push(`${JSON.stringify(key)}: ${valueType}`);
|
|
45
41
|
}
|
|
@@ -61,14 +57,18 @@ const trySpreadPattern = (allOfMembers, refs) => {
|
|
|
61
57
|
return undefined;
|
|
62
58
|
}
|
|
63
59
|
// Extract shape entries from inline object
|
|
64
|
-
const { shapeEntries: entries, shapeTypes: types } = parseObjectShape(member, refs, [
|
|
60
|
+
const { shapeEntries: entries, shapeTypes: types } = parseObjectShape(member, refs, [
|
|
61
|
+
...refs.path,
|
|
62
|
+
"allOf",
|
|
63
|
+
idx,
|
|
64
|
+
]);
|
|
65
65
|
shapeEntries.push(...entries);
|
|
66
66
|
shapeTypes.push(...types);
|
|
67
67
|
}
|
|
68
68
|
if (shapeEntries.length === 0)
|
|
69
69
|
return undefined;
|
|
70
70
|
return {
|
|
71
|
-
expression: `z.
|
|
71
|
+
expression: `z.looseObject({ ${shapeEntries.join(", ")} })`,
|
|
72
72
|
type: `z.ZodObject<{ ${shapeTypes.join(", ")} }>`,
|
|
73
73
|
};
|
|
74
74
|
};
|
|
@@ -79,9 +79,7 @@ const ensureOriginalIndex = (arr) => {
|
|
|
79
79
|
if (typeof item === "boolean") {
|
|
80
80
|
newArr.push(item ? { [originalIndexKey]: i } : { [originalIndexKey]: i, not: {} });
|
|
81
81
|
}
|
|
82
|
-
else if (typeof item === "object" &&
|
|
83
|
-
item !== null &&
|
|
84
|
-
originalIndexKey in item) {
|
|
82
|
+
else if (typeof item === "object" && item !== null && originalIndexKey in item) {
|
|
85
83
|
return arr;
|
|
86
84
|
}
|
|
87
85
|
else {
|
|
@@ -96,7 +94,7 @@ export function parseAllOf(schema, refs) {
|
|
|
96
94
|
}
|
|
97
95
|
else if (schema.allOf.length === 1) {
|
|
98
96
|
const item = schema.allOf[0];
|
|
99
|
-
|
|
97
|
+
const parsed = parseSchema(item, {
|
|
100
98
|
...refs,
|
|
101
99
|
path: [
|
|
102
100
|
...refs.path,
|
|
@@ -104,6 +102,7 @@ export function parseAllOf(schema, refs) {
|
|
|
104
102
|
item[originalIndexKey] ?? 0,
|
|
105
103
|
],
|
|
106
104
|
});
|
|
105
|
+
return parsed;
|
|
107
106
|
}
|
|
108
107
|
else {
|
|
109
108
|
// Try spread pattern first (more efficient than intersection)
|
|
@@ -19,8 +19,8 @@ export const parseAnyOf = (schema, refs) => {
|
|
|
19
19
|
}
|
|
20
20
|
return parseSchema(memberSchema, { ...refs, path: [...refs.path, "anyOf", i] });
|
|
21
21
|
});
|
|
22
|
-
const expressions = members.map(m => m.expression).join(", ");
|
|
23
|
-
const types = members.map(m => m.type).join(", ");
|
|
22
|
+
const expressions = members.map((m) => m.expression).join(", ");
|
|
23
|
+
const types = members.map((m) => m.type).join(", ");
|
|
24
24
|
const expression = `z.union([${expressions}])`;
|
|
25
25
|
// Use readonly tuple for union type annotations (required for recursive type inference)
|
|
26
26
|
const type = `z.ZodUnion<readonly [${types}]>`;
|