@gabrielbryk/json-schema-to-zod 2.14.1 → 2.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @gabrielbryk/json-schema-to-zod
2
2
 
3
+ ## 2.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e25f255: Add configurable strategy for recursive oneOf handling, defaulting to union for recursive catchall cases to avoid Zod xor+lazy validation bugs. Also add serverless workflow e2e validation coverage.
8
+
9
+ ## 2.14.2
10
+
11
+ ### Patch Changes
12
+
13
+ - b4460e4: Restore getter-based recursion for named properties to preserve inferred types in recursive schemas.
14
+
3
15
  ## 2.14.1
4
16
 
5
17
  ### Patch Changes
@@ -1,86 +1,63 @@
1
1
  import { parseSchema } from "../parsers/parseSchema.js";
2
2
  import { expandJsdocs } from "../utils/jsdocs.js";
3
- import { inferTypeFromExpression } from "../utils/schemaRepresentation.js";
3
+ import { collectRefNames, emitExpression, emitType, nodeHasGetter, nodeHasLazy, } from "../utils/schemaRepresentation.js";
4
4
  import { EsmEmitter } from "../utils/esmEmitter.js";
5
5
  import { resolveTypeName } from "../utils/schemaNaming.js";
6
- /**
7
- * Split a z.object({...}).method1().method2() expression into base and method chain.
8
- * This is needed for Rule 2: Don't chain methods on recursive types.
9
- *
10
- * Only splits if the TOP-LEVEL z.object() contains a getter (not nested ones in .and() etc.)
11
- */
12
- const splitObjectMethodChain = (expr) => {
13
- if (!expr.startsWith("z.object(")) {
14
- return { base: expr, methodChain: null };
15
- }
16
- // Find the matching closing brace for z.object({
17
- let depth = 1;
18
- let i = 9; // length of "z.object("
19
- // Find the opening { of the object literal
20
- while (i < expr.length && expr[i] !== "{") {
21
- i++;
22
- }
23
- if (i >= expr.length) {
24
- return { base: expr, methodChain: null };
25
- }
26
- const objectLiteralStart = i;
27
- i++; // move past the {
28
- // Find the matching }
29
- while (i < expr.length && depth > 0) {
30
- const char = expr[i];
31
- if (char === "{" || char === "(" || char === "[") {
32
- depth++;
33
- }
34
- else if (char === "}" || char === ")" || char === "]") {
35
- depth--;
36
- }
37
- i++;
38
- }
39
- const objectLiteralEnd = i - 1; // position of closing }
40
- // Extract just the top-level object literal content
41
- const objectLiteralContent = expr.substring(objectLiteralStart, objectLiteralEnd + 1);
42
- // Check if the TOP-LEVEL object has a getter (not nested ones)
43
- // A getter in the top-level object would appear as "get " at depth 1
44
- if (!hasTopLevelGetter(objectLiteralContent)) {
45
- return { base: expr, methodChain: null };
46
- }
47
- // Now find the closing ) for z.object(
48
- while (i < expr.length && expr[i] !== ")") {
49
- i++;
50
- }
51
- if (i >= expr.length) {
52
- return { base: expr, methodChain: null };
53
- }
54
- i++; // move past the )
55
- // Everything after is the method chain
56
- const base = expr.substring(0, i);
57
- const methodChain = expr.substring(i);
58
- // Only return a method chain if there actually is one (like .strict() or .meta())
59
- // Don't split if the method chain is .and() since that's adding more schema, not metadata
60
- if (!methodChain || methodChain.length === 0 || methodChain.startsWith(".and(")) {
61
- return { base: expr, methodChain: null };
62
- }
63
- return { base, methodChain };
64
- };
65
- /**
66
- * Check if an object literal has a getter at its top level (not nested).
67
- */
68
- const hasTopLevelGetter = (objectLiteral) => {
69
- let depth = 0;
70
- for (let i = 0; i < objectLiteral.length - 4; i++) {
71
- const char = objectLiteral[i];
72
- if (char === "{" || char === "(" || char === "[") {
73
- depth++;
74
- }
75
- else if (char === "}" || char === ")" || char === "]") {
76
- depth--;
77
- }
78
- else if (depth === 1 && objectLiteral.substring(i, i + 4) === "get ") {
79
- // Found "get " at depth 1 (inside the top-level object, not nested)
80
- return true;
6
+ const splitObjectMethodChain = (node) => {
7
+ const chain = [];
8
+ let current = node;
9
+ while (current) {
10
+ switch (current.kind) {
11
+ case "object":
12
+ return {
13
+ base: current,
14
+ methodChain: chain.length ? chain.reverse().join("") : null,
15
+ };
16
+ case "readonly":
17
+ chain.push(".readonly()");
18
+ current = current.inner;
19
+ break;
20
+ case "describe":
21
+ chain.push(`.describe(${JSON.stringify(current.description)})`);
22
+ current = current.inner;
23
+ break;
24
+ case "meta":
25
+ chain.push(`.meta(${current.meta})`);
26
+ current = current.inner;
27
+ break;
28
+ case "default":
29
+ chain.push(`.default(${JSON.stringify(current.value)})`);
30
+ current = current.inner;
31
+ break;
32
+ case "catchall":
33
+ chain.push(`.catchall(${emitExpression(current.catchall)})`);
34
+ current = current.base;
35
+ break;
36
+ case "superRefine":
37
+ chain.push(`.superRefine(${current.refine})`);
38
+ current = current.base;
39
+ break;
40
+ case "refine":
41
+ chain.push(`.refine(${current.refine})`);
42
+ current = current.base;
43
+ break;
44
+ case "transform":
45
+ chain.push(`.transform(${current.transform})`);
46
+ current = current.base;
47
+ break;
48
+ case "pipe":
49
+ chain.push(`.pipe(${emitExpression(current.second)}${current.params ?? ""})`);
50
+ current = current.first;
51
+ break;
52
+ case "chain":
53
+ chain.push(`.${current.method}`);
54
+ current = current.base;
55
+ break;
56
+ default:
57
+ return { base: node, methodChain: null };
81
58
  }
82
59
  }
83
- return false;
60
+ return { base: node, methodChain: null };
84
61
  };
85
62
  const orderDeclarations = (entries, dependencies) => {
86
63
  const repByName = new Map(entries);
@@ -101,16 +78,18 @@ const orderDeclarations = (entries, dependencies) => {
101
78
  onlyKnown.forEach((d) => current.add(d));
102
79
  depGraph.set(from, current);
103
80
  }
104
- // Add regex-detected dependencies from expressions
81
+ // Add dependencies from IR
105
82
  const names = Array.from(repByName.keys());
106
83
  for (const [name, rep] of entries) {
107
84
  const deps = depGraph.get(name) ?? new Set();
108
- for (const candidate of names) {
109
- if (candidate === name)
110
- continue;
111
- const matcher = new RegExp(`\\b${candidate}\\b`);
112
- if (matcher.test(rep.expression)) {
113
- deps.add(candidate);
85
+ if (!rep.node) {
86
+ throw new Error(`Missing IR node for ${name} (no-fallback mode).`);
87
+ }
88
+ const node = rep.node;
89
+ const refs = collectRefNames(node);
90
+ for (const refName of refs) {
91
+ if (refName !== name && repByName.has(refName)) {
92
+ deps.add(refName);
114
93
  }
115
94
  }
116
95
  depGraph.set(name, deps);
@@ -196,44 +175,45 @@ export const emitZod = (analysis) => {
196
175
  }
197
176
  if (declarations.size) {
198
177
  for (const [refName, rep] of orderDeclarations(Array.from(declarations.entries()), dependencies)) {
199
- const expression = typeof rep === "string" ? rep : rep.expression;
200
- if (typeof expression !== "string") {
201
- throw new Error(`Expected declaration expression for ${refName}`);
178
+ if (!rep.node) {
179
+ throw new Error(`Missing IR node for ${refName} (no-fallback mode).`);
202
180
  }
203
- const hintedType = typeof rep === "object" &&
204
- rep &&
205
- "type" in rep &&
206
- typeof rep.type === "string"
207
- ? rep.type
208
- : undefined;
181
+ const node = rep.node;
182
+ const expression = emitExpression(node);
183
+ const hintedType = rep.type;
209
184
  const effectiveHint = hintedType === "z.ZodTypeAny" ? undefined : hintedType;
210
- const hasLazy = expression.includes("z.lazy(");
211
- const hasGetter = expression.includes("get ");
185
+ const hasLazy = nodeHasLazy(node);
186
+ const hasGetter = nodeHasGetter(node);
212
187
  // Check if this schema references any cycle members (recursive schemas)
213
188
  // This can cause TS7056 when TypeScript tries to serialize the expanded type
214
- const referencesRecursiveSchema = Array.from(cycleRefNames).some((cycleName) => new RegExp(`\\b${cycleName}\\b`).test(expression));
189
+ let referencesRecursiveSchema = false;
190
+ const refs = collectRefNames(node);
191
+ for (const refName of refs) {
192
+ if (cycleRefNames.has(refName)) {
193
+ referencesRecursiveSchema = true;
194
+ break;
195
+ }
196
+ }
215
197
  // Per Zod v4 docs: type annotations should be on GETTERS for recursive types, not on const declarations.
216
198
  // TypeScript can infer the type of const declarations.
217
199
  // Exceptions that need explicit type annotation:
218
200
  // 1. z.lazy() without getters
219
201
  // 2. Any schema that references recursive schemas (to prevent TS7056)
220
202
  const needsTypeAnnotation = (hasLazy && !hasGetter) || referencesRecursiveSchema;
221
- const storedType = needsTypeAnnotation
222
- ? (effectiveHint ?? inferTypeFromExpression(expression))
223
- : undefined;
203
+ const storedType = needsTypeAnnotation ? (effectiveHint ?? emitType(node)) : undefined;
224
204
  // Rule 2 from Zod v4: Don't chain methods on recursive types
225
205
  // If the schema has getters (recursive), we need to split it:
226
206
  // 1. Emit base schema as _RefName
227
207
  // 2. Emit decorated schema as RefName = _RefName.methods()
228
- if (hasGetter && expression.startsWith("z.object(")) {
229
- const { base, methodChain } = splitObjectMethodChain(expression);
208
+ if (hasGetter) {
209
+ const { base, methodChain } = splitObjectMethodChain(node);
230
210
  if (methodChain) {
231
211
  // Emit base schema (internal, not exported)
232
212
  // No type annotation needed - type is on getters, TypeScript infers the rest
233
213
  const baseName = `_${refName}`;
234
214
  emitter.addConst({
235
215
  name: baseName,
236
- expression: base,
216
+ expression: emitExpression(base),
237
217
  exported: false,
238
218
  });
239
219
  // Emit decorated schema (exported)
@@ -1,6 +1,8 @@
1
1
  import { analyzeSchema } from "../core/analyzeSchema.js";
2
2
  import { emitZod } from "../core/emitZod.js";
3
3
  import { liftInlineObjects } from "../utils/liftInlineObjects.js";
4
+ import { anyOrUnknown } from "../utils/anyOrUnknown.js";
5
+ import { zodLazy, zodRef } from "../utils/schemaRepresentation.js";
4
6
  export const generateSchemaBundle = (schema, options = {}) => {
5
7
  if (!schema || typeof schema !== "object") {
6
8
  throw new Error("generateSchemaBundle requires an object schema");
@@ -137,16 +139,20 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
137
139
  path: refs.path,
138
140
  isCycle,
139
141
  });
140
- if (resolved)
142
+ if (resolved) {
143
+ if (!resolved.node) {
144
+ throw new Error("refResolution.onRef must return SchemaRepresentation with node (no-fallback mode).");
145
+ }
141
146
  return resolved;
147
+ }
142
148
  // Self-recursion ALWAYS needs z.lazy if not using getters
143
149
  if (refName === currentDefName) {
144
- return `z.lazy(() => ${refInfo.schemaName})`;
150
+ return zodLazy(refInfo.schemaName);
145
151
  }
146
152
  if (isCycle && useLazyCrossRefs) {
147
- return `z.lazy(() => ${refInfo.schemaName})`;
153
+ return zodLazy(refInfo.schemaName);
148
154
  }
149
- return refInfo.schemaName;
155
+ return zodRef(refInfo.schemaName);
150
156
  }
151
157
  // If it's NOT exactly a top-level definition, it could be:
152
158
  // 1. A path into a top-level definition (e.g. #/$defs/alpha/properties/foo)
@@ -159,9 +165,13 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
159
165
  ref: refPath,
160
166
  currentDef: currentDefName,
161
167
  });
162
- if (unknown)
168
+ if (unknown) {
169
+ if (!unknown.node) {
170
+ throw new Error("refResolution.onUnknownRef must return SchemaRepresentation with node (no-fallback mode).");
171
+ }
163
172
  return unknown;
164
- return options.useUnknown ? "z.unknown()" : "z.any()";
173
+ }
174
+ return anyOrUnknown(refs);
165
175
  }
166
176
  return undefined;
167
177
  };
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ export * from "./utils/resolveUri.js";
39
39
  export * from "./utils/schemaNaming.js";
40
40
  export * from "./utils/schemaRepresentation.js";
41
41
  export * from "./utils/withMessage.js";
42
+ export * from "./utils/wrapRecursiveUnion.js";
42
43
  export * from "./zodToJsonSchema.js";
43
44
  import { jsonSchemaToZod } from "./jsonSchemaToZod.js";
44
45
  export default jsonSchemaToZod;
@@ -1,5 +1,6 @@
1
1
  import { parseSchema } from "./parseSchema.js";
2
2
  import { half } from "../utils/half.js";
3
+ import { shouldUseGetter, zodExactOptional, zodIntersection, zodLooseObject, zodNever, } from "../utils/schemaRepresentation.js";
3
4
  const originalIndexKey = "__originalIndex";
4
5
  /**
5
6
  * Check if a schema defines object properties (inline object shape) without any refs.
@@ -22,7 +23,6 @@ const isInlineObjectOnly = (schema) => {
22
23
  */
23
24
  const parseObjectShape = (schema, refs, pathPrefix) => {
24
25
  const shapeEntries = [];
25
- const shapeTypes = [];
26
26
  for (const key of Object.keys(schema.properties)) {
27
27
  const propSchema = schema.properties[key];
28
28
  const parsedProp = parseSchema(propSchema, {
@@ -34,12 +34,11 @@ 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 ? `${parsedProp.expression}.exactOptional()` : parsedProp.expression;
38
- const valueType = optional ? `z.ZodExactOptional<${parsedProp.type}>` : parsedProp.type;
39
- shapeEntries.push(`${JSON.stringify(key)}: ${valueExpr}`);
40
- shapeTypes.push(`${JSON.stringify(key)}: ${valueType}`);
37
+ const valueRep = optional ? zodExactOptional(parsedProp) : parsedProp;
38
+ const isGetter = shouldUseGetter(valueRep, refs.currentSchemaName, refs.cycleRefNames, refs.cycleComponentByName);
39
+ shapeEntries.push({ key, rep: valueRep, isGetter });
41
40
  }
42
- return { shapeEntries, shapeTypes };
41
+ return { shapeEntries };
43
42
  };
44
43
  /**
45
44
  * Check if all allOf members can be combined using spread syntax.
@@ -48,7 +47,6 @@ const parseObjectShape = (schema, refs, pathPrefix) => {
48
47
  */
49
48
  const trySpreadPattern = (allOfMembers, refs) => {
50
49
  const shapeEntries = [];
51
- const shapeTypes = [];
52
50
  for (let i = 0; i < allOfMembers.length; i++) {
53
51
  const member = allOfMembers[i];
54
52
  const idx = member[originalIndexKey] ?? i;
@@ -57,20 +55,12 @@ const trySpreadPattern = (allOfMembers, refs) => {
57
55
  return undefined;
58
56
  }
59
57
  // Extract shape entries from inline object
60
- const { shapeEntries: entries, shapeTypes: types } = parseObjectShape(member, refs, [
61
- ...refs.path,
62
- "allOf",
63
- idx,
64
- ]);
58
+ const { shapeEntries: entries } = parseObjectShape(member, refs, [...refs.path, "allOf", idx]);
65
59
  shapeEntries.push(...entries);
66
- shapeTypes.push(...types);
67
60
  }
68
61
  if (shapeEntries.length === 0)
69
62
  return undefined;
70
- return {
71
- expression: `z.looseObject({ ${shapeEntries.join(", ")} })`,
72
- type: `z.ZodObject<{ ${shapeTypes.join(", ")} }>`,
73
- };
63
+ return zodLooseObject(shapeEntries);
74
64
  };
75
65
  const ensureOriginalIndex = (arr) => {
76
66
  const newArr = [];
@@ -90,7 +80,7 @@ const ensureOriginalIndex = (arr) => {
90
80
  };
91
81
  export function parseAllOf(schema, refs) {
92
82
  if (schema.allOf.length === 0) {
93
- return { expression: "z.never()", type: "z.ZodNever" };
83
+ return zodNever();
94
84
  }
95
85
  else if (schema.allOf.length === 1) {
96
86
  const item = schema.allOf[0];
@@ -116,9 +106,6 @@ export function parseAllOf(schema, refs) {
116
106
  const [left, right] = half(indexed);
117
107
  const leftResult = parseAllOf({ allOf: left }, refs);
118
108
  const rightResult = parseAllOf({ allOf: right }, refs);
119
- return {
120
- expression: `z.intersection(${leftResult.expression}, ${rightResult.expression})`,
121
- type: `z.ZodIntersection<${leftResult.type}, ${rightResult.type}>`,
122
- };
109
+ return zodIntersection(leftResult, rightResult);
123
110
  }
124
111
  }
@@ -2,6 +2,8 @@ import { parseSchema } from "./parseSchema.js";
2
2
  import { anyOrUnknown } from "../utils/anyOrUnknown.js";
3
3
  import { extractInlineObject } from "../utils/extractInlineObject.js";
4
4
  import { normalizeUnionMembers } from "../utils/normalizeUnion.js";
5
+ import { wrapRecursiveUnion } from "../utils/wrapRecursiveUnion.js";
6
+ import { zodRef, zodUnion } from "../utils/schemaRepresentation.js";
5
7
  export const parseAnyOf = (schema, refs) => {
6
8
  if (!schema.anyOf.length) {
7
9
  return anyOrUnknown(refs);
@@ -16,7 +18,7 @@ export const parseAnyOf = (schema, refs) => {
16
18
  const members = schema.anyOf.map((memberSchema, i) => {
17
19
  const extracted = extractInlineObject(memberSchema, refs, [...refs.path, "anyOf", i]);
18
20
  if (extracted) {
19
- return { expression: extracted, type: `typeof ${extracted}` };
21
+ return zodRef(extracted);
20
22
  }
21
23
  return parseSchema(memberSchema, { ...refs, path: [...refs.path, "anyOf", i] });
22
24
  });
@@ -27,10 +29,6 @@ export const parseAnyOf = (schema, refs) => {
27
29
  if (normalized.length === 1) {
28
30
  return normalized[0];
29
31
  }
30
- const expressions = normalized.map((m) => m.expression).join(", ");
31
- const types = normalized.map((m) => m.type).join(", ");
32
- const expression = `z.union([${expressions}])`;
33
- // Use readonly tuple for union type annotations (required for recursive type inference)
34
- const type = `z.ZodUnion<readonly [${types}]>`;
35
- return { expression, type };
32
+ const union = zodUnion(normalized, { readonlyType: true });
33
+ return wrapRecursiveUnion(refs, union);
36
34
  };
@@ -1,6 +1,7 @@
1
1
  import { withMessage } from "../utils/withMessage.js";
2
2
  import { parseSchema } from "./parseSchema.js";
3
3
  import { anyOrUnknown } from "../utils/anyOrUnknown.js";
4
+ import { zodArray, zodChain, zodSuperRefine, zodTuple } from "../utils/schemaRepresentation.js";
4
5
  export const parseArray = (schema, refs) => {
5
6
  // JSON Schema 2020-12 uses `prefixItems` for tuples.
6
7
  // Older drafts used `items` as an array.
@@ -8,33 +9,29 @@ export const parseArray = (schema, refs) => {
8
9
  if (prefixItems) {
9
10
  // Tuple case
10
11
  const itemResults = prefixItems.map((v, i) => parseSchema(v, { ...refs, path: [...refs.path, "prefixItems", i] }));
11
- let tuple = `z.tuple([${itemResults.map((r) => r.expression).join(", ")}])`;
12
- // We construct the type manually for the tuple part
13
- let tupleTypes = itemResults.map((r) => r.type).join(", ");
14
- let tupleType = `z.ZodTuple<[${tupleTypes}], null>`; // Default null rest
15
12
  // Handle "additionalItems" (older drafts) or "items" (2020-12 when prefixItems is used)
16
13
  // If prefixItems is present, `items` acts as the schema for additional items.
17
14
  // If prefixItems came from `items` (array form), then `additionalItems` controls the rest.
18
15
  const additionalSchema = schema.prefixItems ? schema.items : schema.additionalItems;
16
+ let rest;
19
17
  if (additionalSchema === false) {
20
18
  // Closed tuple
19
+ rest = null;
21
20
  }
22
21
  else if (additionalSchema) {
23
- const restSchema = additionalSchema === true
24
- ? anyOrUnknown(refs)
25
- : parseSchema(additionalSchema, {
26
- ...refs,
27
- path: [...refs.path, "items"],
28
- });
29
- tuple += `.rest(${restSchema.expression})`;
30
- tupleType = `z.ZodTuple<[${tupleTypes}], ${restSchema.type}>`;
22
+ rest =
23
+ additionalSchema === true
24
+ ? anyOrUnknown(refs)
25
+ : parseSchema(additionalSchema, {
26
+ ...refs,
27
+ path: [...refs.path, "items"],
28
+ });
31
29
  }
32
30
  else {
33
31
  // Open by default
34
- const anyRes = anyOrUnknown(refs);
35
- tuple += `.rest(${anyRes.expression})`;
36
- tupleType = `z.ZodTuple<[${tupleTypes}], ${anyRes.type}>`;
32
+ rest = anyOrUnknown(refs);
37
33
  }
34
+ let result = zodTuple(itemResults, rest);
38
35
  if (schema.contains) {
39
36
  const containsResult = parseSchema(schema.contains, {
40
37
  ...refs,
@@ -42,7 +39,7 @@ export const parseArray = (schema, refs) => {
42
39
  });
43
40
  const minContains = schema.minContains ?? (schema.contains ? 1 : undefined);
44
41
  const maxContains = schema.maxContains;
45
- tuple += `.superRefine((arr, ctx) => {
42
+ result = zodSuperRefine(result, `(arr, ctx) => {
46
43
  const matches = arr.filter((item) => ${containsResult.expression}.safeParse(item).success).length;
47
44
  if (${minContains ?? 0} && matches < ${minContains ?? 0}) {
48
45
  ctx.addIssue({ code: "custom", message: "Array contains too few matching items" });
@@ -50,12 +47,9 @@ export const parseArray = (schema, refs) => {
50
47
  if (${maxContains ?? "undefined"} !== undefined && matches > ${maxContains ?? "undefined"}) {
51
48
  ctx.addIssue({ code: "custom", message: "Array contains too many matching items" });
52
49
  }
53
- })`;
50
+ }`);
54
51
  }
55
- return {
56
- expression: tuple,
57
- type: tupleType,
58
- };
52
+ return result;
59
53
  }
60
54
  // Regular Array case
61
55
  const itemsSchema = schema.items;
@@ -66,22 +60,27 @@ export const parseArray = (schema, refs) => {
66
60
  ...refs,
67
61
  path: [...refs.path, "items"],
68
62
  });
69
- let r = `z.array(${itemResult.expression})`;
70
- let arrayType = `z.ZodArray<${itemResult.type}>`;
71
- r += withMessage(schema, "minItems", ({ json }) => ({
63
+ let result = zodArray(itemResult);
64
+ const minItems = withMessage(schema, "minItems", ({ json }) => ({
72
65
  opener: `.min(${json}`,
73
66
  closer: ")",
74
67
  messagePrefix: ", { message: ",
75
68
  messageCloser: " })",
76
69
  }));
77
- r += withMessage(schema, "maxItems", ({ json }) => ({
70
+ if (minItems) {
71
+ result = zodChain(result, minItems.slice(1));
72
+ }
73
+ const maxItems = withMessage(schema, "maxItems", ({ json }) => ({
78
74
  opener: `.max(${json}`,
79
75
  closer: ")",
80
76
  messagePrefix: ", { message: ",
81
77
  messageCloser: " })",
82
78
  }));
79
+ if (maxItems) {
80
+ result = zodChain(result, maxItems.slice(1));
81
+ }
83
82
  if (schema.uniqueItems === true) {
84
- r += `.superRefine((arr, ctx) => {
83
+ result = zodSuperRefine(result, `(arr, ctx) => {
85
84
  const seen = new Set();
86
85
  for (const [index, value] of arr.entries()) {
87
86
  let key;
@@ -102,7 +101,7 @@ export const parseArray = (schema, refs) => {
102
101
 
103
102
  seen.add(key);
104
103
  }
105
- })`;
104
+ }`);
106
105
  }
107
106
  if (schema.contains) {
108
107
  const containsResult = parseSchema(schema.contains, {
@@ -111,7 +110,7 @@ export const parseArray = (schema, refs) => {
111
110
  });
112
111
  const minContains = schema.minContains ?? (schema.contains ? 1 : undefined);
113
112
  const maxContains = schema.maxContains;
114
- r += `.superRefine((arr, ctx) => {
113
+ result = zodSuperRefine(result, `(arr, ctx) => {
115
114
  const matches = arr.filter((item) => ${containsResult.expression}.safeParse(item).success).length;
116
115
  if (${minContains ?? 0} && matches < ${minContains ?? 0}) {
117
116
  ctx.addIssue({ code: "custom", message: "Array contains too few matching items" });
@@ -119,10 +118,7 @@ export const parseArray = (schema, refs) => {
119
118
  if (${maxContains ?? "undefined"} !== undefined && matches > ${maxContains ?? "undefined"}) {
120
119
  ctx.addIssue({ code: "custom", message: "Array contains too many matching items" });
121
120
  }
122
- })`;
121
+ }`);
123
122
  }
124
- return {
125
- expression: r,
126
- type: arrayType,
127
- };
123
+ return result;
128
124
  };
@@ -1,4 +1,2 @@
1
- export const parseBoolean = () => ({
2
- expression: "z.boolean()",
3
- type: "z.ZodBoolean",
4
- });
1
+ import { zodBoolean } from "../utils/schemaRepresentation.js";
2
+ export const parseBoolean = () => zodBoolean();
@@ -1,22 +1,4 @@
1
+ import { zodLiteral } from "../utils/schemaRepresentation.js";
1
2
  export const parseConst = (schema) => {
2
- const value = schema.const;
3
- const expression = `z.literal(${JSON.stringify(value)})`;
4
- // Determine the literal type based on the value type
5
- let type;
6
- if (typeof value === "string") {
7
- type = `z.ZodLiteral<${JSON.stringify(value)}>`;
8
- }
9
- else if (typeof value === "number") {
10
- type = `z.ZodLiteral<${value}>`;
11
- }
12
- else if (typeof value === "boolean") {
13
- type = `z.ZodLiteral<${value}>`;
14
- }
15
- else if (value === null) {
16
- type = "z.ZodLiteral<null>";
17
- }
18
- else {
19
- type = "z.ZodLiteral<unknown>";
20
- }
21
- return { expression, type };
3
+ return zodLiteral(schema.const);
22
4
  };
@@ -1,35 +1,20 @@
1
+ import { zodEnum, zodLiteral, zodNever, zodUnion } from "../utils/schemaRepresentation.js";
1
2
  export const parseEnum = (schema) => {
2
3
  if (schema.enum.length === 0) {
3
- return {
4
- expression: "z.never()",
5
- type: "z.ZodNever",
6
- };
4
+ return zodNever();
7
5
  }
8
6
  else if (schema.enum.length === 1) {
9
7
  // union does not work when there is only one element
10
8
  const value = schema.enum[0];
11
- return {
12
- expression: `z.literal(${JSON.stringify(value)})`,
13
- type: `z.ZodLiteral<${typeof value === "string" ? JSON.stringify(value) : value}>`,
14
- };
9
+ return zodLiteral(value);
15
10
  }
16
11
  else if (schema.enum.every((x) => typeof x === "string")) {
17
12
  const values = schema.enum;
18
13
  // Zod v4 ZodEnum uses object format: { key: "key"; ... }
19
- const enumObject = values.map((x) => `${JSON.stringify(x)}: ${JSON.stringify(x)}`).join("; ");
20
- return {
21
- expression: `z.enum([${values.map((x) => JSON.stringify(x))}])`,
22
- type: `z.ZodEnum<{ ${enumObject} }>`,
23
- };
14
+ return zodEnum(values, { typeStyle: "object" });
24
15
  }
25
16
  else {
26
17
  // Mixed types: create union of literals
27
- const literalTypes = schema.enum.map((x) => typeof x === "string" ? JSON.stringify(x) : x === null ? "null" : String(x));
28
- return {
29
- expression: `z.union([${schema.enum
30
- .map((x) => `z.literal(${JSON.stringify(x)})`)
31
- .join(", ")}])`,
32
- type: `z.ZodUnion<[${literalTypes.map((t) => `z.ZodLiteral<${t}>`).join(", ")}]>`,
33
- };
18
+ return zodUnion(schema.enum.map((value) => zodLiteral(value)));
34
19
  }
35
20
  };