@gabrielbryk/json-schema-to-zod 2.10.1 → 2.11.1

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 +38 -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 +26 -13
  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 +10 -6
  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 +200 -37
  25. package/dist/parsers/parseOneOf.js +365 -0
  26. package/dist/{esm/parsers → parsers}/parseSchema.js +55 -117
  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 -157
  79. package/dist/cjs/generators/generateBundle.js +0 -510
  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 -318
  97. package/dist/cjs/parsers/parseOneOf.js +0 -53
  98. package/dist/cjs/parsers/parseSchema.js +0 -419
  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 -153
  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
@@ -0,0 +1,92 @@
1
+ import { resolveUri } from "./resolveUri.js";
2
+ import { buildRefRegistry } from "./buildRefRegistry.js";
3
+ const decodePointerSegment = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
4
+ const uriBaseFromRef = (resolvedUri) => {
5
+ const hashIdx = resolvedUri.indexOf("#");
6
+ return hashIdx === -1 ? resolvedUri : resolvedUri.slice(0, hashIdx);
7
+ };
8
+ const isLocalBase = (base, rootBase) => {
9
+ if (!rootBase)
10
+ return false;
11
+ return base === rootBase;
12
+ };
13
+ export const resolveRef = (schemaNode, ref, refs) => {
14
+ const base = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
15
+ // Handle dynamicRef lookup via dynamicAnchors stack
16
+ const isDynamic = typeof schemaNode.$dynamicRef === "string";
17
+ if (isDynamic && refs.dynamicAnchors && ref.startsWith("#")) {
18
+ const name = ref.slice(1);
19
+ for (let i = refs.dynamicAnchors.length - 1; i >= 0; i -= 1) {
20
+ const entry = refs.dynamicAnchors[i];
21
+ if (entry.name === name) {
22
+ const key = `${entry.uri}#${name}`;
23
+ const target = refs.refRegistry?.get(key);
24
+ if (target) {
25
+ return { schema: target.schema, path: target.path, baseUri: target.baseUri, pointerKey: key };
26
+ }
27
+ }
28
+ }
29
+ }
30
+ // Resolve URI against base
31
+ const resolvedUri = resolveUri(base, ref);
32
+ const [uriBase, fragment] = resolvedUri.split("#");
33
+ const key = fragment ? `${uriBase}#${fragment}` : uriBase;
34
+ let regEntry = refs.refRegistry?.get(key);
35
+ if (regEntry) {
36
+ return { schema: regEntry.schema, path: regEntry.path, baseUri: regEntry.baseUri, pointerKey: key };
37
+ }
38
+ // Legacy recursive ref: treat as dynamic to __recursive__
39
+ if (schemaNode.$recursiveRef) {
40
+ const recursiveKey = `${base}#__recursive__`;
41
+ regEntry = refs.refRegistry?.get(recursiveKey);
42
+ if (regEntry) {
43
+ return {
44
+ schema: regEntry.schema,
45
+ path: regEntry.path,
46
+ baseUri: regEntry.baseUri,
47
+ pointerKey: recursiveKey,
48
+ };
49
+ }
50
+ }
51
+ // External resolver hook
52
+ const extBase = uriBaseFromRef(resolvedUri);
53
+ if (refs.resolveExternalRef && extBase && !isLocalBase(extBase, refs.rootBaseUri ?? "")) {
54
+ const loaded = refs.resolveExternalRef(extBase);
55
+ if (loaded) {
56
+ // If async resolver is used synchronously here, it will be ignored; keep simple sync for now
57
+ const maybePromise = loaded;
58
+ const schema = typeof maybePromise.then === "function"
59
+ ? undefined
60
+ : loaded;
61
+ if (schema) {
62
+ const { registry } = buildRefRegistry(schema, extBase);
63
+ registry.forEach((entry, k) => refs.refRegistry?.set(k, entry));
64
+ regEntry = refs.refRegistry?.get(key);
65
+ if (regEntry) {
66
+ return {
67
+ schema: regEntry.schema,
68
+ path: regEntry.path,
69
+ baseUri: regEntry.baseUri,
70
+ pointerKey: key,
71
+ };
72
+ }
73
+ }
74
+ }
75
+ }
76
+ // Backward compatibility: JSON Pointer into root
77
+ if (refs.root && ref.startsWith("#/")) {
78
+ const rawSegments = ref
79
+ .slice(2)
80
+ .split("/")
81
+ .filter((segment) => segment.length > 0)
82
+ .map(decodePointerSegment);
83
+ let current = refs.root;
84
+ for (const segment of rawSegments) {
85
+ if (typeof current !== "object" || current === null)
86
+ return undefined;
87
+ current = current[segment];
88
+ }
89
+ return { schema: current, path: rawSegments, baseUri: base, pointerKey: ref };
90
+ }
91
+ return undefined;
92
+ };
@@ -0,0 +1,569 @@
1
+ /**
2
+ * Builder functions for composing SchemaRepresentation objects.
3
+ * These track both the Zod expression and its TypeScript type simultaneously.
4
+ */
5
+ // Primitives
6
+ export const zodString = () => ({
7
+ expression: "z.string()",
8
+ type: "z.ZodString",
9
+ });
10
+ export const zodNumber = () => ({
11
+ expression: "z.number()",
12
+ type: "z.ZodNumber",
13
+ });
14
+ export const zodBoolean = () => ({
15
+ expression: "z.boolean()",
16
+ type: "z.ZodBoolean",
17
+ });
18
+ export const zodNull = () => ({
19
+ expression: "z.null()",
20
+ type: "z.ZodNull",
21
+ });
22
+ export const zodUndefined = () => ({
23
+ expression: "z.undefined()",
24
+ type: "z.ZodUndefined",
25
+ });
26
+ export const zodAny = () => ({
27
+ expression: "z.any()",
28
+ type: "z.ZodAny",
29
+ });
30
+ export const zodUnknown = () => ({
31
+ expression: "z.unknown()",
32
+ type: "z.ZodUnknown",
33
+ });
34
+ export const zodNever = () => ({
35
+ expression: "z.never()",
36
+ type: "z.ZodNever",
37
+ });
38
+ export const zodBigInt = () => ({
39
+ expression: "z.bigint()",
40
+ type: "z.ZodBigInt",
41
+ });
42
+ export const zodDate = () => ({
43
+ expression: "z.date()",
44
+ type: "z.ZodDate",
45
+ });
46
+ // Reference to another schema (potentially recursive)
47
+ export const zodRef = (schemaName) => ({
48
+ expression: schemaName,
49
+ type: `typeof ${schemaName}`,
50
+ });
51
+ // Lazy wrapper for recursive references
52
+ export const zodLazy = (schemaName) => ({
53
+ expression: `z.lazy(() => ${schemaName})`,
54
+ type: `z.ZodLazy<typeof ${schemaName}>`,
55
+ });
56
+ // Typed lazy wrapper when we know the inner type
57
+ export const zodLazyTyped = (schemaName, innerType) => ({
58
+ expression: `z.lazy<${innerType}>(() => ${schemaName})`,
59
+ type: `z.ZodLazy<${innerType}>`,
60
+ });
61
+ // Wrappers that transform inner representations
62
+ export const zodArray = (inner) => ({
63
+ expression: `z.array(${inner.expression})`,
64
+ type: `z.ZodArray<${inner.type}>`,
65
+ });
66
+ export const zodOptional = (inner) => ({
67
+ expression: `${inner.expression}.optional()`,
68
+ type: `z.ZodOptional<${inner.type}>`,
69
+ });
70
+ export const zodNullable = (inner) => ({
71
+ expression: `${inner.expression}.nullable()`,
72
+ type: `z.ZodNullable<${inner.type}>`,
73
+ });
74
+ export const zodNullableWrapper = (inner) => ({
75
+ expression: `z.nullable(${inner.expression})`,
76
+ type: `z.ZodNullable<${inner.type}>`,
77
+ });
78
+ export const zodDefault = (inner, defaultValue) => ({
79
+ expression: `${inner.expression}.default(${defaultValue})`,
80
+ type: `z.ZodDefault<${inner.type}>`,
81
+ });
82
+ export const zodReadonly = (inner) => ({
83
+ expression: `${inner.expression}.readonly()`,
84
+ type: `z.ZodReadonly<${inner.type}>`,
85
+ });
86
+ // Describe doesn't change the type
87
+ export const zodDescribe = (inner, description) => ({
88
+ expression: `${inner.expression}.describe(${JSON.stringify(description)})`,
89
+ type: inner.type,
90
+ });
91
+ // Meta doesn't change the type
92
+ export const zodMeta = (inner, meta) => ({
93
+ expression: `${inner.expression}.meta(${meta})`,
94
+ type: inner.type,
95
+ });
96
+ // Literals
97
+ export const zodLiteral = (value) => ({
98
+ expression: `z.literal(${value})`,
99
+ type: `z.ZodLiteral<${value}>`,
100
+ });
101
+ // Enums
102
+ export const zodEnum = (values) => {
103
+ const valuesStr = `[${values.join(", ")}]`;
104
+ return {
105
+ expression: `z.enum(${valuesStr})`,
106
+ type: `z.ZodEnum<${valuesStr}>`,
107
+ };
108
+ };
109
+ // Union
110
+ export const zodUnion = (options) => {
111
+ const exprs = options.map((o) => o.expression).join(", ");
112
+ const types = options.map((o) => o.type).join(", ");
113
+ return {
114
+ expression: `z.union([${exprs}])`,
115
+ type: `z.ZodUnion<[${types}]>`,
116
+ };
117
+ };
118
+ // Discriminated union
119
+ export const zodDiscriminatedUnion = (discriminator, options) => {
120
+ const exprs = options.map((o) => o.expression).join(", ");
121
+ const types = options.map((o) => o.type).join(", ");
122
+ return {
123
+ expression: `z.discriminatedUnion(${JSON.stringify(discriminator)}, [${exprs}])`,
124
+ type: `z.ZodDiscriminatedUnion<${JSON.stringify(discriminator)}, [${types}]>`,
125
+ };
126
+ };
127
+ // Intersection
128
+ export const zodIntersection = (left, right) => ({
129
+ expression: `z.intersection(${left.expression}, ${right.expression})`,
130
+ type: `z.ZodIntersection<${left.type}, ${right.type}>`,
131
+ });
132
+ // And method (for chaining)
133
+ export const zodAnd = (base, other) => ({
134
+ expression: `${base.expression}.and(${other.expression})`,
135
+ type: `z.ZodIntersection<${base.type}, ${other.type}>`,
136
+ });
137
+ // Tuple
138
+ export const zodTuple = (items) => {
139
+ const exprs = items.map((i) => i.expression).join(", ");
140
+ const types = items.map((i) => i.type).join(", ");
141
+ return {
142
+ expression: `z.tuple([${exprs}])`,
143
+ type: `z.ZodTuple<[${types}]>`,
144
+ };
145
+ };
146
+ // Record
147
+ export const zodRecord = (key, value) => ({
148
+ expression: `z.record(${key.expression}, ${value.expression})`,
149
+ type: `z.ZodRecord<${key.type}, ${value.type}>`,
150
+ });
151
+ // Map
152
+ export const zodMap = (key, value) => ({
153
+ expression: `z.map(${key.expression}, ${value.expression})`,
154
+ type: `z.ZodMap<${key.type}, ${value.type}>`,
155
+ });
156
+ // Set
157
+ export const zodSet = (value) => ({
158
+ expression: `z.set(${value.expression})`,
159
+ type: `z.ZodSet<${value.type}>`,
160
+ });
161
+ // Object - builds from shape entries
162
+ export const zodObject = (shape) => {
163
+ const exprParts = [];
164
+ const typeParts = [];
165
+ for (const { key, rep, isGetter } of shape) {
166
+ const quotedKey = JSON.stringify(key);
167
+ if (isGetter) {
168
+ // Getter syntax with explicit type annotation
169
+ exprParts.push(`get ${quotedKey}(): ${rep.type} { return ${rep.expression} }`);
170
+ }
171
+ else {
172
+ exprParts.push(`${quotedKey}: ${rep.expression}`);
173
+ }
174
+ typeParts.push(`${quotedKey}: ${rep.type}`);
175
+ }
176
+ return {
177
+ expression: `z.object({ ${exprParts.join(", ")} })`,
178
+ type: `z.ZodObject<{ ${typeParts.join(", ")} }>`,
179
+ };
180
+ };
181
+ // Strict object
182
+ export const zodStrictObject = (shape) => {
183
+ const base = zodObject(shape);
184
+ return {
185
+ expression: `${base.expression}.strict()`,
186
+ type: base.type, // strict() doesn't change the type signature
187
+ };
188
+ };
189
+ // Catchall
190
+ export const zodCatchall = (base, catchallSchema) => ({
191
+ expression: `${base.expression}.catchall(${catchallSchema.expression})`,
192
+ type: base.type, // catchall doesn't change the base type for inference purposes
193
+ });
194
+ // SuperRefine - doesn't change the type
195
+ export const zodSuperRefine = (base, refineFn) => ({
196
+ expression: `${base.expression}.superRefine(${refineFn})`,
197
+ type: base.type,
198
+ });
199
+ // Refine - doesn't change the type
200
+ export const zodRefine = (base, refineFn) => ({
201
+ expression: `${base.expression}.refine(${refineFn})`,
202
+ type: base.type,
203
+ });
204
+ // Transform - Zod v4 uses ZodPipe<Base, ZodTransform<Output, Input>>
205
+ // Since we don't know the output type at codegen time, use ZodTypeAny for simplicity
206
+ export const zodTransform = (base, transformFn) => ({
207
+ expression: `${base.expression}.transform(${transformFn})`,
208
+ type: `z.ZodPipe<${base.type}, z.ZodTypeAny>`,
209
+ });
210
+ // Pipe
211
+ export const zodPipe = (first, second) => ({
212
+ expression: `${first.expression}.pipe(${second.expression})`,
213
+ type: `z.ZodPipeline<${first.type}, ${second.type}>`,
214
+ });
215
+ // Coerce wrappers
216
+ export const zodCoerceString = () => ({
217
+ expression: "z.coerce.string()",
218
+ type: "z.ZodString",
219
+ });
220
+ export const zodCoerceNumber = () => ({
221
+ expression: "z.coerce.number()",
222
+ type: "z.ZodNumber",
223
+ });
224
+ export const zodCoerceBoolean = () => ({
225
+ expression: "z.coerce.boolean()",
226
+ type: "z.ZodBoolean",
227
+ });
228
+ export const zodCoerceDate = () => ({
229
+ expression: "z.coerce.date()",
230
+ type: "z.ZodDate",
231
+ });
232
+ // Generic method chaining - for any method that doesn't change type
233
+ export const zodChain = (base, method) => ({
234
+ expression: `${base.expression}.${method}`,
235
+ type: base.type,
236
+ });
237
+ // Create a raw representation from expression string (for backward compatibility)
238
+ // This infers the type from the expression using pattern matching
239
+ export const fromExpression = (expression) => ({
240
+ expression,
241
+ type: inferTypeFromExpression(expression),
242
+ });
243
+ /**
244
+ * Infers the TypeScript type from a Zod expression string.
245
+ * This is used for backward compatibility during migration.
246
+ */
247
+ export const inferTypeFromExpression = (expr) => {
248
+ // Handle z.lazy with explicit type (possibly with method chains like .optional())
249
+ const lazyTypedMatch = expr.match(/^z\.lazy<([^>]+)>\(\s*\(\)\s*=>\s*([A-Za-z0-9_.$]+)\s*\)(\.[a-z]+\(\))*$/);
250
+ if (lazyTypedMatch) {
251
+ let type = `z.ZodLazy<${lazyTypedMatch[1]}>`;
252
+ const methods = lazyTypedMatch[3] || "";
253
+ if (methods.includes(".optional()")) {
254
+ type = `z.ZodOptional<${type}>`;
255
+ }
256
+ if (methods.includes(".nullable()")) {
257
+ type = `z.ZodNullable<${type}>`;
258
+ }
259
+ return type;
260
+ }
261
+ // Handle z.lazy without explicit type (possibly with method chains like .optional())
262
+ const lazyMatch = expr.match(/^z\.lazy\(\s*\(\)\s*=>\s*([A-Za-z0-9_.$]+)\s*\)(\.[a-z]+\(\))*$/);
263
+ if (lazyMatch) {
264
+ let type = `z.ZodLazy<typeof ${lazyMatch[1]}>`;
265
+ const methods = lazyMatch[2] || "";
266
+ if (methods.includes(".optional()")) {
267
+ type = `z.ZodOptional<${type}>`;
268
+ }
269
+ if (methods.includes(".nullable()")) {
270
+ type = `z.ZodNullable<${type}>`;
271
+ }
272
+ return type;
273
+ }
274
+ // Handle .and() method chains - this creates an intersection type
275
+ // Need to find the .and( that's not inside nested parentheses
276
+ const andIndex = findTopLevelMethod(expr, ".and(");
277
+ if (andIndex !== -1) {
278
+ const baseExpr = expr.substring(0, andIndex);
279
+ // Extract the argument to .and() - find the matching closing paren
280
+ const argsStart = andIndex + 5; // length of ".and("
281
+ const argsEnd = findMatchingParen(expr, argsStart - 1);
282
+ if (argsEnd !== -1) {
283
+ const andArg = expr.substring(argsStart, argsEnd);
284
+ const remainder = expr.substring(argsEnd + 1);
285
+ const baseType = inferTypeFromExpression(baseExpr);
286
+ const andType = inferTypeFromExpression(andArg);
287
+ let type = `z.ZodIntersection<${baseType}, ${andType}>`;
288
+ // Handle trailing methods
289
+ if (remainder.includes(".optional()")) {
290
+ type = `z.ZodOptional<${type}>`;
291
+ }
292
+ if (remainder.includes(".nullable()")) {
293
+ type = `z.ZodNullable<${type}>`;
294
+ }
295
+ return type;
296
+ }
297
+ }
298
+ // Handle z.intersection(X, Y)
299
+ if (expr.startsWith("z.intersection(")) {
300
+ const argsStart = 15; // length of "z.intersection("
301
+ const argsEnd = findMatchingParen(expr, argsStart - 1);
302
+ if (argsEnd !== -1) {
303
+ const args = expr.substring(argsStart, argsEnd);
304
+ // Split on comma at top level (not inside parentheses)
305
+ const commaIndex = findTopLevelComma(args);
306
+ if (commaIndex !== -1) {
307
+ const leftExpr = args.substring(0, commaIndex).trim();
308
+ const rightExpr = args.substring(commaIndex + 1).trim();
309
+ const leftType = inferTypeFromExpression(leftExpr);
310
+ const rightType = inferTypeFromExpression(rightExpr);
311
+ return `z.ZodIntersection<${leftType}, ${rightType}>`;
312
+ }
313
+ }
314
+ }
315
+ // Handle z.object({...}) - for objects with getters or complex shapes
316
+ if (expr.startsWith("z.object(")) {
317
+ // Find the end of z.object({...})
318
+ const argsStart = 9; // length of "z.object("
319
+ const argsEnd = findMatchingParen(expr, argsStart - 1);
320
+ if (argsEnd !== -1) {
321
+ const remainder = expr.substring(argsEnd + 1);
322
+ // Base type for any z.object
323
+ let type = "z.ZodObject<Record<string, z.ZodTypeAny>>";
324
+ // Handle method chains after z.object({...})
325
+ if (remainder.includes(".strict()")) {
326
+ // .strict() doesn't change the type
327
+ }
328
+ if (remainder.includes(".optional()")) {
329
+ type = `z.ZodOptional<${type}>`;
330
+ }
331
+ if (remainder.includes(".nullable()")) {
332
+ type = `z.ZodNullable<${type}>`;
333
+ }
334
+ return type;
335
+ }
336
+ }
337
+ // Handle z.record(K, V)
338
+ if (expr.startsWith("z.record(")) {
339
+ const argsStart = 9; // length of "z.record("
340
+ const argsEnd = findMatchingParen(expr, argsStart - 1);
341
+ if (argsEnd !== -1) {
342
+ const args = expr.substring(argsStart, argsEnd);
343
+ const commaIndex = findTopLevelComma(args);
344
+ if (commaIndex !== -1) {
345
+ const keyExpr = args.substring(0, commaIndex).trim();
346
+ const valueExpr = args.substring(commaIndex + 1).trim();
347
+ const keyType = inferTypeFromExpression(keyExpr);
348
+ const valueType = inferTypeFromExpression(valueExpr);
349
+ return `z.ZodRecord<${keyType}, ${valueType}>`;
350
+ }
351
+ }
352
+ }
353
+ // Primitives - MUST come before refMatch which would incorrectly match z.string() as "typeof z"
354
+ if (expr === "z.string()" || expr.startsWith("z.string()."))
355
+ return "z.ZodString";
356
+ if (expr === "z.number()" || expr.startsWith("z.number()."))
357
+ return "z.ZodNumber";
358
+ if (expr === "z.boolean()" || expr.startsWith("z.boolean()."))
359
+ return "z.ZodBoolean";
360
+ if (expr === "z.null()")
361
+ return "z.ZodNull";
362
+ if (expr === "z.undefined()")
363
+ return "z.ZodUndefined";
364
+ if (expr === "z.any()")
365
+ return "z.ZodAny";
366
+ if (expr === "z.unknown()")
367
+ return "z.ZodUnknown";
368
+ if (expr === "z.never()")
369
+ return "z.ZodNever";
370
+ if (expr.startsWith("z.literal("))
371
+ return "z.ZodLiteral<unknown>";
372
+ if (expr.startsWith("z.enum("))
373
+ return "z.ZodEnum<[string, ...string[]]>";
374
+ // Handle simple schema reference (possibly with .optional())
375
+ const refMatch = expr.match(/^([A-Za-z_$][A-Za-z0-9_$]*)(\.[a-z]+\(\))*$/);
376
+ if (refMatch) {
377
+ const baseName = refMatch[1];
378
+ const methods = refMatch[2] || "";
379
+ let type = `typeof ${baseName}`;
380
+ if (methods.includes(".optional()")) {
381
+ type = `z.ZodOptional<${type}>`;
382
+ }
383
+ if (methods.includes(".nullable()")) {
384
+ type = `z.ZodNullable<${type}>`;
385
+ }
386
+ return type;
387
+ }
388
+ // Handle z.array(X)
389
+ const arrayMatch = expr.match(/^z\.array\((.+)\)(\.[a-z]+\(\))*$/);
390
+ if (arrayMatch) {
391
+ const innerType = inferTypeFromExpression(arrayMatch[1]);
392
+ let type = `z.ZodArray<${innerType}>`;
393
+ const methods = arrayMatch[2] || "";
394
+ if (methods.includes(".optional()")) {
395
+ type = `z.ZodOptional<${type}>`;
396
+ }
397
+ if (methods.includes(".nullable()")) {
398
+ type = `z.ZodNullable<${type}>`;
399
+ }
400
+ return type;
401
+ }
402
+ // Handle z.nullable(X)
403
+ const nullableMatch = expr.match(/^z\.nullable\((.+)\)$/);
404
+ if (nullableMatch) {
405
+ const innerType = inferTypeFromExpression(nullableMatch[1]);
406
+ return `z.ZodNullable<${innerType}>`;
407
+ }
408
+ // Handle z.union([...]) - Zod v4 uses readonly arrays for union options
409
+ // Also handle method chains like .optional(), .nullable()
410
+ if (expr.startsWith("z.union([")) {
411
+ const bracketStart = 8; // position of [
412
+ const bracketEnd = findMatchingParen(expr, bracketStart); // position of ]
413
+ if (bracketEnd !== -1) {
414
+ const arrayContent = expr.substring(bracketStart + 1, bracketEnd); // inside the []
415
+ const memberTypes = parseTopLevelArrayElements(arrayContent);
416
+ const types = memberTypes.map(m => inferTypeFromExpression(m.trim()));
417
+ let baseType = `z.ZodUnion<readonly [${types.join(", ")}]>`;
418
+ const remainder = expr.substring(bracketEnd + 2); // skip ] and )
419
+ if (remainder.includes(".optional()")) {
420
+ baseType = `z.ZodOptional<${baseType}>`;
421
+ }
422
+ if (remainder.includes(".nullable()")) {
423
+ baseType = `z.ZodNullable<${baseType}>`;
424
+ }
425
+ return baseType;
426
+ }
427
+ }
428
+ // Handle z.discriminatedUnion(...) - Zod v4 uses readonly arrays
429
+ if (expr.startsWith("z.discriminatedUnion(")) {
430
+ let baseType = "z.ZodDiscriminatedUnion<readonly z.ZodTypeAny[], string>";
431
+ if (expr.endsWith(".optional()")) {
432
+ baseType = `z.ZodOptional<${baseType}>`;
433
+ }
434
+ if (expr.endsWith(".nullable()")) {
435
+ baseType = `z.ZodNullable<${baseType}>`;
436
+ }
437
+ return baseType;
438
+ }
439
+ // Fallback
440
+ return "z.ZodTypeAny";
441
+ };
442
+ /**
443
+ * Find a method call at the top level (not inside nested parentheses)
444
+ */
445
+ const findTopLevelMethod = (expr, method) => {
446
+ let depth = 0;
447
+ for (let i = 0; i < expr.length - method.length; i++) {
448
+ if (expr[i] === '(' || expr[i] === '[' || expr[i] === '{') {
449
+ depth++;
450
+ }
451
+ else if (expr[i] === ')' || expr[i] === ']' || expr[i] === '}') {
452
+ depth--;
453
+ }
454
+ else if (depth === 0 && expr.substring(i, i + method.length) === method) {
455
+ return i;
456
+ }
457
+ }
458
+ return -1;
459
+ };
460
+ /**
461
+ * Find the matching closing parenthesis
462
+ */
463
+ const findMatchingParen = (expr, openIndex) => {
464
+ let depth = 0;
465
+ for (let i = openIndex; i < expr.length; i++) {
466
+ if (expr[i] === '(' || expr[i] === '[' || expr[i] === '{') {
467
+ depth++;
468
+ }
469
+ else if (expr[i] === ')' || expr[i] === ']' || expr[i] === '}') {
470
+ depth--;
471
+ if (depth === 0) {
472
+ return i;
473
+ }
474
+ }
475
+ }
476
+ return -1;
477
+ };
478
+ /**
479
+ * Find a comma at the top level (not inside nested parentheses)
480
+ */
481
+ const findTopLevelComma = (expr) => {
482
+ let depth = 0;
483
+ for (let i = 0; i < expr.length; i++) {
484
+ if (expr[i] === '(' || expr[i] === '[' || expr[i] === '{') {
485
+ depth++;
486
+ }
487
+ else if (expr[i] === ')' || expr[i] === ']' || expr[i] === '}') {
488
+ depth--;
489
+ }
490
+ else if (depth === 0 && expr[i] === ',') {
491
+ return i;
492
+ }
493
+ }
494
+ return -1;
495
+ };
496
+ /**
497
+ * Parse array elements at the top level, respecting nested brackets/parens
498
+ */
499
+ const parseTopLevelArrayElements = (content) => {
500
+ const elements = [];
501
+ let depth = 0;
502
+ let current = "";
503
+ for (let i = 0; i < content.length; i++) {
504
+ const char = content[i];
505
+ if (char === '(' || char === '[' || char === '{') {
506
+ depth++;
507
+ current += char;
508
+ }
509
+ else if (char === ')' || char === ']' || char === '}') {
510
+ depth--;
511
+ current += char;
512
+ }
513
+ else if (char === ',' && depth === 0) {
514
+ if (current.trim()) {
515
+ elements.push(current.trim());
516
+ }
517
+ current = "";
518
+ }
519
+ else {
520
+ current += char;
521
+ }
522
+ }
523
+ if (current.trim()) {
524
+ elements.push(current.trim());
525
+ }
526
+ return elements;
527
+ };
528
+ /**
529
+ * Check if an expression contains a reference to a recursive schema.
530
+ */
531
+ export const containsRecursiveRef = (expr, cycleRefNames) => {
532
+ if (!cycleRefNames || cycleRefNames.size === 0)
533
+ return false;
534
+ for (const refName of cycleRefNames) {
535
+ // Check for direct reference or reference within z.lazy, z.array, etc.
536
+ const pattern = new RegExp(`\\b${refName}\\b`);
537
+ if (pattern.test(expr)) {
538
+ return true;
539
+ }
540
+ }
541
+ return false;
542
+ };
543
+ /**
544
+ * Determines if a property should use getter syntax based on its representation
545
+ * and the current schema context.
546
+ */
547
+ export const shouldUseGetter = (rep, currentSchemaName, cycleRefNames, cycleComponentByName) => {
548
+ if (!currentSchemaName)
549
+ return false;
550
+ // Check if the expression directly references the current schema (self-recursion)
551
+ if (rep.expression === currentSchemaName)
552
+ return true;
553
+ // Check if expression contains a reference to a cycle member in the same SCC
554
+ if (!cycleRefNames || cycleRefNames.size === 0)
555
+ return false;
556
+ const currentComponent = cycleComponentByName?.get(currentSchemaName);
557
+ if (currentComponent === undefined)
558
+ return false;
559
+ for (const refName of cycleRefNames) {
560
+ const pattern = new RegExp(`\\b${refName}\\b`);
561
+ if (pattern.test(rep.expression)) {
562
+ const refComponent = cycleComponentByName?.get(refName);
563
+ if (refComponent === currentComponent) {
564
+ return true;
565
+ }
566
+ }
567
+ }
568
+ return false;
569
+ };