@gabrielbryk/json-schema-to-zod 2.9.0 → 2.10.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.
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildRefRegistry = void 0;
4
4
  const resolveUri_js_1 = require("./resolveUri.js");
5
- const buildRefRegistry = (schema, rootBaseUri = "root:///", opts = {}) => {
5
+ const buildRefRegistry = (schema, rootBaseUri = "root:///") => {
6
6
  const registry = new Map();
7
7
  const walk = (node, baseUri, path) => {
8
8
  if (typeof node !== "object" || node === null)
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.omit = void 0;
4
4
  const omit = (obj, ...keys) => Object.keys(obj).reduce((acc, key) => {
5
- if (!keys.includes(key)) {
6
- acc[key] = obj[key];
5
+ const typedKey = key;
6
+ if (!keys.includes(typedKey)) {
7
+ acc[typedKey] = obj[typedKey];
7
8
  }
8
9
  return acc;
9
10
  }, {});
@@ -70,6 +70,16 @@ export const emitZod = (analysis) => {
70
70
  const { module, name, type, noImport, exportRefs, withMeta, ...rest } = options;
71
71
  const declarations = new Map();
72
72
  const dependencies = new Map();
73
+ const reserveName = (base) => {
74
+ let candidate = base;
75
+ let i = 1;
76
+ while (usedNames.has(candidate) || declarations.has(candidate)) {
77
+ candidate = `${base}${i}`;
78
+ i += 1;
79
+ }
80
+ usedNames.add(candidate);
81
+ return candidate;
82
+ };
73
83
  const parsedSchema = parseSchema(schema, {
74
84
  module,
75
85
  name,
@@ -91,15 +101,21 @@ export const emitZod = (analysis) => {
91
101
  });
92
102
  const declarationBlock = declarations.size
93
103
  ? orderDeclarations(Array.from(declarations.entries()), dependencies)
94
- .map(([refName, value]) => {
104
+ .flatMap(([refName, value]) => {
95
105
  const shouldExport = exportRefs && module === "esm";
96
- const decl = `${shouldExport ? "export " : ""}const ${refName} = ${value}`;
97
- return decl;
106
+ const isCycle = cycleRefNames.has(refName);
107
+ if (!isCycle) {
108
+ return [`${shouldExport ? "export " : ""}const ${refName} = ${value}`];
109
+ }
110
+ const baseName = `${refName}Def`;
111
+ const lines = [`const ${baseName} = ${value}`];
112
+ lines.push(`${shouldExport ? "export " : ""}const ${refName} = ${baseName}`);
113
+ return lines;
98
114
  })
99
115
  .join("\n")
100
116
  : "";
101
- const jsdocs = rest.withJsdocs && typeof schema !== "boolean" && schema.description
102
- ? expandJsdocs(schema.description)
117
+ const jsdocs = rest.withJsdocs && typeof schema === "object" && schema !== null && "description" in schema
118
+ ? expandJsdocs(String(schema.description ?? ""))
103
119
  : "";
104
120
  const lines = [];
105
121
  if (module === "cjs" && !noImport) {
@@ -8,23 +8,27 @@ export const generateSchemaBundle = (schema, options = {}) => {
8
8
  const defs = schema.$defs || {};
9
9
  const definitions = schema.definitions || {};
10
10
  const defNames = Object.keys(defs);
11
- const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
11
+ const { rootName, rootTypeName, defInfoMap, groups } = buildBundleContext(defNames, defs, options);
12
12
  const files = [];
13
- const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, rootTypeName);
13
+ const targets = planBundleTargets(schema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
14
14
  for (const target of targets) {
15
15
  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);
16
+ const zodParts = [];
17
+ for (const member of target.members) {
18
+ const analysis = analyzeSchema(member.schemaWithDefs, {
19
+ ...options,
20
+ module,
21
+ name: member.schemaName,
22
+ type: member.typeName,
23
+ parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, {
24
+ ...(member.schemaWithDefs.$defs || {}),
25
+ ...(member.schemaWithDefs.definitions || {}),
26
+ }, options, target.groupId),
27
+ });
28
+ const zodSchema = emitZod(analysis);
29
+ zodParts.push(zodSchema);
30
+ }
31
+ const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap, module, target);
28
32
  files.push({ fileName: target.fileName, contents: finalSchema });
29
33
  }
30
34
  // Nested types extraction (optional)
@@ -53,13 +57,16 @@ const buildDefInfoMap = (defNames, defs, options) => {
53
57
  const pascalName = toPascalCase(defName);
54
58
  const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
55
59
  const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
60
+ const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
56
61
  map.set(defName, {
57
62
  name: defName,
58
63
  pascalName,
59
64
  schemaName,
60
65
  typeName,
66
+ fileName,
61
67
  dependencies,
62
68
  hasCycle: false,
69
+ groupId: "",
63
70
  });
64
71
  }
65
72
  return map;
@@ -72,13 +79,22 @@ const buildBundleContext = (defNames, defs, options) => {
72
79
  if (info)
73
80
  info.hasCycle = true;
74
81
  }
82
+ const groups = buildSccGroups(defInfoMap);
83
+ for (const [groupId, members] of groups) {
84
+ for (const defName of members) {
85
+ const info = defInfoMap.get(defName);
86
+ if (info)
87
+ info.groupId = groupId;
88
+ }
89
+ }
75
90
  const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
76
91
  const rootTypeName = typeof options.type === "string"
77
92
  ? options.type
78
93
  : options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
79
- return { defInfoMap, rootName, rootTypeName };
94
+ return { defInfoMap, rootName, rootTypeName, groups };
80
95
  };
81
- const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
96
+ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options, currentGroupId) => {
97
+ const useLazyCrossRefs = options.refResolution?.lazyCrossRefs ?? true;
82
98
  return (schema, refs) => {
83
99
  if (typeof schema["$ref"] === "string") {
84
100
  const refPath = schema["$ref"];
@@ -92,7 +108,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
92
108
  const refInfo = defInfoMap.get(refName);
93
109
  if (refInfo) {
94
110
  // Track imports when referencing other defs
95
- if (refName !== currentDefName) {
111
+ if (refName !== currentDefName && refInfo.groupId !== currentGroupId) {
96
112
  usedRefs.add(refName);
97
113
  }
98
114
  const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
@@ -105,7 +121,14 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
105
121
  });
106
122
  if (resolved)
107
123
  return resolved;
108
- if (isCycle && options.refResolution?.lazyCrossRefs) {
124
+ if (isCycle && useLazyCrossRefs) {
125
+ const inObjectProperty = refs.path.includes("properties") ||
126
+ refs.path.includes("patternProperties") ||
127
+ refs.path.includes("additionalProperties");
128
+ if (inObjectProperty && refName === currentDefName) {
129
+ // Self-recursion inside object getters can safely reference the schema name
130
+ return refInfo.schemaName;
131
+ }
109
132
  return `z.lazy(() => ${refInfo.schemaName})`;
110
133
  }
111
134
  return refInfo.schemaName;
@@ -124,42 +147,73 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
124
147
  return undefined;
125
148
  };
126
149
  };
127
- const buildSchemaFile = (zodCode, usedRefs, defInfoMap, module) => {
150
+ const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap, module, target) => {
128
151
  if (module !== "esm")
129
- return zodCode;
130
- const imports = [];
152
+ return zodCodeParts.join("\n");
153
+ const groupFileById = new Map();
154
+ for (const info of defInfoMap.values()) {
155
+ if (!groupFileById.has(info.groupId)) {
156
+ groupFileById.set(info.groupId, info.fileName.replace(/\.ts$/, ".js"));
157
+ }
158
+ }
159
+ const importsByFile = new Map();
131
160
  for (const refName of [...usedRefs].sort()) {
132
161
  const refInfo = defInfoMap.get(refName);
133
162
  if (refInfo) {
134
- imports.push(`import { ${refInfo.schemaName} } from './${refName}.schema.js';`);
163
+ const groupFile = groupFileById.get(refInfo.groupId) ?? refInfo.fileName.replace(/\.ts$/, ".js");
164
+ const path = `./${groupFile}`;
165
+ const set = importsByFile.get(path) ?? new Set();
166
+ set.add(refInfo.schemaName);
167
+ importsByFile.set(path, set);
135
168
  }
136
169
  }
137
- if (!imports.length)
138
- return zodCode;
139
- return zodCode.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`);
170
+ const imports = [];
171
+ for (const [path, names] of [...importsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
172
+ imports.push(`import { ${[...names].sort().join(", ")} } from '${path}';`);
173
+ }
174
+ const body = zodCodeParts
175
+ .map((code, idx) => {
176
+ if (idx === 0)
177
+ return code;
178
+ return code.replace(/^import \{ z \} from "zod"\n?/, "");
179
+ })
180
+ .join("\n");
181
+ return imports.length
182
+ ? body.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`)
183
+ : body;
140
184
  };
141
- const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, rootTypeName) => {
185
+ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
142
186
  const targets = [];
187
+ const groupById = new Map();
143
188
  for (const defName of defNames) {
144
- const defSchema = defs[defName];
145
- const defSchemaWithDefs = {
146
- ...defSchema,
147
- $defs: { ...defs, ...defSchema?.$defs },
148
- definitions: {
149
- ...defSchema.definitions,
150
- ...definitions,
151
- },
152
- };
153
- const pascalName = toPascalCase(defName);
154
- const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
155
- const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
156
- const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
189
+ const info = defInfoMap.get(defName);
190
+ const gid = info?.groupId || defName;
191
+ if (!groupById.has(gid))
192
+ groupById.set(gid, []);
193
+ groupById.get(gid).push(defName);
194
+ }
195
+ for (const [groupId, memberDefs] of groupById.entries()) {
196
+ const orderedDefs = orderGroupMembers(memberDefs, defInfoMap);
197
+ const members = orderedDefs.map((defName) => {
198
+ const defSchema = defs[defName];
199
+ const defSchemaWithDefs = {
200
+ ...defSchema,
201
+ $defs: { ...defs, ...defSchema?.$defs },
202
+ definitions: {
203
+ ...defSchema.definitions,
204
+ ...definitions,
205
+ },
206
+ };
207
+ const pascalName = toPascalCase(defName);
208
+ const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
209
+ const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
210
+ return { defName, schemaWithDefs: defSchemaWithDefs, schemaName, typeName };
211
+ });
212
+ const fileName = defInfoMap.get(memberDefs[0])?.fileName ?? `${memberDefs[0]}.schema.ts`;
157
213
  targets.push({
158
- defName,
159
- schemaWithDefs: defSchemaWithDefs,
160
- schemaName,
161
- typeName,
214
+ groupId,
162
215
  fileName,
216
+ members,
163
217
  usedRefs: new Set(),
164
218
  isRoot: false,
165
219
  });
@@ -167,17 +221,22 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
167
221
  if (options.splitDefs?.includeRoot ?? true) {
168
222
  const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
169
223
  targets.push({
170
- defName: null,
171
- schemaWithDefs: {
172
- ...rootSchema,
173
- definitions: {
174
- ...rootSchema.definitions,
175
- ...definitions,
176
- },
177
- },
178
- schemaName: rootName,
179
- typeName: rootTypeName,
224
+ groupId: "root",
180
225
  fileName: rootFile,
226
+ members: [
227
+ {
228
+ defName: null,
229
+ schemaWithDefs: {
230
+ ...rootSchema,
231
+ definitions: {
232
+ ...rootSchema.definitions,
233
+ ...definitions,
234
+ },
235
+ },
236
+ schemaName: rootName,
237
+ typeName: rootTypeName,
238
+ },
239
+ ],
181
240
  usedRefs: new Set(),
182
241
  isRoot: true,
183
242
  });
@@ -208,6 +267,35 @@ const findRefDependencies = (schema, validDefNames) => {
208
267
  traverse(schema);
209
268
  return deps;
210
269
  };
270
+ const orderGroupMembers = (defs, defInfoMap) => {
271
+ const inGroup = new Set(defs);
272
+ const visited = new Set();
273
+ const temp = new Set();
274
+ const result = [];
275
+ const visit = (name) => {
276
+ if (visited.has(name))
277
+ return;
278
+ if (temp.has(name)) {
279
+ return;
280
+ }
281
+ temp.add(name);
282
+ const info = defInfoMap.get(name);
283
+ if (info) {
284
+ for (const dep of info.dependencies) {
285
+ if (inGroup.has(dep)) {
286
+ visit(dep);
287
+ }
288
+ }
289
+ }
290
+ temp.delete(name);
291
+ visited.add(name);
292
+ result.push(name);
293
+ };
294
+ for (const name of defs) {
295
+ visit(name);
296
+ }
297
+ return result;
298
+ };
211
299
  const detectCycles = (defInfoMap) => {
212
300
  const cycleNodes = new Set();
213
301
  const visited = new Set();
@@ -241,6 +329,52 @@ const detectCycles = (defInfoMap) => {
241
329
  }
242
330
  return cycleNodes;
243
331
  };
332
+ const buildSccGroups = (defInfoMap) => {
333
+ const indexMap = new Map();
334
+ const lowLink = new Map();
335
+ const onStack = new Set();
336
+ const stack = [];
337
+ let index = 0;
338
+ const groups = new Map();
339
+ const strongConnect = (node) => {
340
+ indexMap.set(node, index);
341
+ lowLink.set(node, index);
342
+ index += 1;
343
+ stack.push(node);
344
+ onStack.add(node);
345
+ const info = defInfoMap.get(node);
346
+ if (info) {
347
+ for (const dep of info.dependencies) {
348
+ if (!indexMap.has(dep)) {
349
+ strongConnect(dep);
350
+ lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(dep)));
351
+ }
352
+ else if (onStack.has(dep)) {
353
+ lowLink.set(node, Math.min(lowLink.get(node), indexMap.get(dep)));
354
+ }
355
+ }
356
+ }
357
+ if (lowLink.get(node) === indexMap.get(node)) {
358
+ const members = [];
359
+ let w;
360
+ do {
361
+ w = stack.pop();
362
+ if (w) {
363
+ onStack.delete(w);
364
+ members.push(w);
365
+ }
366
+ } while (w && w !== node);
367
+ const groupId = members.sort().join("__");
368
+ groups.set(groupId, members);
369
+ }
370
+ };
371
+ for (const name of defInfoMap.keys()) {
372
+ if (!indexMap.has(name)) {
373
+ strongConnect(name);
374
+ }
375
+ }
376
+ return groups;
377
+ };
244
378
  const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
245
379
  const allNestedTypes = [];
246
380
  for (const defName of defNames) {
@@ -285,7 +419,7 @@ const findNestedTypesInSchema = (schema, parentTypeName, defNames, currentPath =
285
419
  }
286
420
  // inline $defs
287
421
  if (record.$defs && typeof record.$defs === "object") {
288
- for (const [_defName, defSchema] of Object.entries(record.$defs)) {
422
+ for (const [, defSchema] of Object.entries(record.$defs)) {
289
423
  nestedTypes.push(...findNestedTypesInSchema(defSchema, parentTypeName, defNames, currentPath));
290
424
  }
291
425
  }
@@ -316,6 +450,26 @@ const generateNestedTypesFile = (nestedTypes) => {
316
450
  " * They are extracted using TypeScript indexed access types.",
317
451
  " */",
318
452
  "",
453
+ "type Access<T, P extends readonly (string | number)[]> =",
454
+ " P extends []",
455
+ " ? NonNullable<T>",
456
+ " : P extends readonly [infer H, ...infer R]",
457
+ " ? H extends \"items\"",
458
+ " ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
459
+ " : H extends \"additionalProperties\"",
460
+ " ? Access<NonNullable<T> extends Record<string, infer V> ? V : unknown, Extract<R, (string | number)[]>>",
461
+ " : H extends number",
462
+ " ? Access<NonNullable<T> extends Array<infer U> ? U : unknown, Extract<R, (string | number)[]>>",
463
+ " : H extends string",
464
+ " ? Access<",
465
+ " H extends keyof NonNullable<T>",
466
+ " ? NonNullable<NonNullable<T>[H]>",
467
+ " : unknown,",
468
+ " Extract<R, (string | number)[]>",
469
+ " >",
470
+ " : unknown",
471
+ " : unknown;",
472
+ "",
319
473
  ];
320
474
  const byParent = new Map();
321
475
  for (const info of nestedTypes) {
@@ -334,14 +488,15 @@ const generateNestedTypesFile = (nestedTypes) => {
334
488
  lines.push(`import type { ${typeName} } from './${file}.schema.js';`);
335
489
  }
336
490
  lines.push("");
491
+ const buildAccessExpr = (parentType, propertyPath) => {
492
+ const path = propertyPath.map((prop) => (typeof prop === "number" ? prop : JSON.stringify(prop))).join(", ");
493
+ return `Access<${parentType}, [${path}]>`;
494
+ };
337
495
  for (const [parentType, types] of [...byParent.entries()].sort()) {
338
496
  lines.push(`// From ${parentType}`);
339
497
  for (const info of types.sort((a, b) => a.typeName.localeCompare(b.typeName))) {
340
498
  if (info.propertyPath.length > 0) {
341
- let accessExpr = parentType;
342
- for (const prop of info.propertyPath) {
343
- accessExpr = `NonNullable<${accessExpr}['${prop}']>`;
344
- }
499
+ const accessExpr = buildAccessExpr(parentType, info.propertyPath);
345
500
  lines.push(`export type ${info.typeName} = ${accessExpr};`);
346
501
  }
347
502
  }
@@ -1,18 +1,20 @@
1
1
  import { parseSchema } from "./parseSchema.js";
2
2
  import { half } from "../utils/half.js";
3
- const originalIndex = Symbol("Original index");
3
+ const originalIndexKey = "__originalIndex";
4
4
  const ensureOriginalIndex = (arr) => {
5
- let newArr = [];
5
+ const newArr = [];
6
6
  for (let i = 0; i < arr.length; i++) {
7
7
  const item = arr[i];
8
8
  if (typeof item === "boolean") {
9
- newArr.push(item ? { [originalIndex]: i } : { [originalIndex]: i, not: {} });
9
+ newArr.push(item ? { [originalIndexKey]: i } : { [originalIndexKey]: i, not: {} });
10
10
  }
11
- else if (originalIndex in item) {
11
+ else if (typeof item === "object" &&
12
+ item !== null &&
13
+ originalIndexKey in item) {
12
14
  return arr;
13
15
  }
14
16
  else {
15
- newArr.push({ ...item, [originalIndex]: i });
17
+ newArr.push({ ...item, [originalIndexKey]: i });
16
18
  }
17
19
  }
18
20
  return newArr;
@@ -25,7 +27,11 @@ export function parseAllOf(schema, refs) {
25
27
  const item = schema.allOf[0];
26
28
  return parseSchema(item, {
27
29
  ...refs,
28
- path: [...refs.path, "allOf", item[originalIndex]],
30
+ path: [
31
+ ...refs.path,
32
+ "allOf",
33
+ item[originalIndexKey] ?? 0,
34
+ ],
29
35
  });
30
36
  }
31
37
  else {
@@ -1,3 +1 @@
1
- export const parseBoolean = (_schema) => {
2
- return "z.boolean()";
3
- };
1
+ export const parseBoolean = () => "z.boolean()";
@@ -14,8 +14,8 @@ export const parseIfThenElse = (schema, refs) => {
14
14
  ? ${$then}.safeParse(value)
15
15
  : ${$else}.safeParse(value);
16
16
  if (!result.success) {
17
- const issues = result.error.issues ?? result.error.errors ?? [];
18
- issues.forEach((issue) => ctx.addIssue(issue))
17
+ const issues = result.error.issues;
18
+ issues.forEach((issue) => ctx.addIssue({ ...issue }))
19
19
  }
20
20
  })`;
21
21
  // Store original if/then/else for JSON Schema round-trip
@@ -1,3 +1 @@
1
- export const parseNull = (_schema) => {
2
- return "z.null()";
3
- };
1
+ export const parseNull = () => "z.null()";
@@ -197,6 +197,7 @@ export function parseObject(objectSchema, refs) {
197
197
  output += `.and(${parseAnyOf({
198
198
  ...objectSchema,
199
199
  anyOf: objectSchema.anyOf.map((x) => typeof x === "object" &&
200
+ x !== null &&
200
201
  !x.type &&
201
202
  (x.properties || x.additionalProperties || x.patternProperties)
202
203
  ? { ...x, type: "object" }
@@ -207,6 +208,7 @@ export function parseObject(objectSchema, refs) {
207
208
  output += `.and(${parseOneOf({
208
209
  ...objectSchema,
209
210
  oneOf: objectSchema.oneOf.map((x) => typeof x === "object" &&
211
+ x !== null &&
210
212
  !x.type &&
211
213
  (x.properties || x.additionalProperties || x.patternProperties)
212
214
  ? { ...x, type: "object" }
@@ -217,6 +219,7 @@ export function parseObject(objectSchema, refs) {
217
219
  output += `.and(${parseAllOf({
218
220
  ...objectSchema,
219
221
  allOf: objectSchema.allOf.map((x) => typeof x === "object" &&
222
+ x !== null &&
220
223
  !x.type &&
221
224
  (x.properties || x.additionalProperties || x.patternProperties)
222
225
  ? { ...x, type: "object" }
@@ -230,6 +233,7 @@ export function parseObject(objectSchema, refs) {
230
233
  // propertyNames
231
234
  if (objectSchema.propertyNames) {
232
235
  const normalizedPropNames = typeof objectSchema.propertyNames === "object" &&
236
+ objectSchema.propertyNames !== null &&
233
237
  !objectSchema.propertyNames.type &&
234
238
  objectSchema.propertyNames.pattern
235
239
  ? { ...objectSchema.propertyNames, type: "string" }
@@ -258,12 +262,12 @@ export function parseObject(objectSchema, refs) {
258
262
  if (entries.length) {
259
263
  output += `.superRefine((obj, ctx) => {
260
264
  ${entries
261
- .map(([key, schema], idx) => {
265
+ .map(([key, schema]) => {
262
266
  const parsed = parseSchema(schema, { ...refs, path: [...refs.path, "dependentSchemas", key] });
263
267
  return `if (Object.prototype.hasOwnProperty.call(obj, ${JSON.stringify(key)})) {
264
268
  const result = ${parsed}.safeParse(obj);
265
269
  if (!result.success) {
266
- ctx.addIssue({ code: "custom", message: "Dependent schema failed", path: [], params: { issues: result.error.issues } });
270
+ ctx.addIssue({ code: "custom", message: ${objectSchema.errorMessage?.dependentSchemas ?? JSON.stringify("Dependent schema failed")}, path: [], params: { issues: result.error.issues } });
267
271
  }
268
272
  }`;
269
273
  })
@@ -275,7 +279,8 @@ export function parseObject(objectSchema, refs) {
275
279
  if (objectSchema.dependentRequired && typeof objectSchema.dependentRequired === "object") {
276
280
  const entries = Object.entries(objectSchema.dependentRequired);
277
281
  if (entries.length) {
278
- const depRequiredMessage = objectSchema.errorMessage?.dependentRequired ?? "Dependent required properties missing";
282
+ const depRequiredMessage = objectSchema.errorMessage?.dependentRequired ??
283
+ "Dependent required properties missing";
279
284
  output += `.superRefine((obj, ctx) => {
280
285
  ${entries
281
286
  .map(([prop, deps]) => {
@@ -303,5 +308,8 @@ const shouldUseGetter = (parsed, refs) => {
303
308
  if (refs.currentSchemaName && parsed.includes(refs.currentSchemaName)) {
304
309
  return true;
305
310
  }
311
+ if (refs.cycleRefNames?.has(parsed)) {
312
+ return true;
313
+ }
306
314
  return Boolean(refs.inProgress && refs.inProgress.has(parsed));
307
315
  };
@@ -30,9 +30,7 @@ export const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockM
30
30
  if (typeof schema !== "object")
31
31
  return schema ? anyOrUnknown(refs) : "z.never()";
32
32
  const parentBase = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
33
- const baseUri = typeof schema.$id === "string"
34
- ? resolveUri(parentBase, schema.$id)
35
- : parentBase;
33
+ const baseUri = typeof schema.$id === "string" ? resolveUri(parentBase, schema.$id) : parentBase;
36
34
  const dynamicAnchors = Array.isArray(refs.dynamicAnchors) ? [...refs.dynamicAnchors] : [];
37
35
  if (typeof schema.$dynamicAnchor === "string") {
38
36
  dynamicAnchors.push({
@@ -81,6 +79,9 @@ export const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockM
81
79
  };
82
80
  const parseRef = (schema, refs) => {
83
81
  const refValue = schema.$dynamicRef ?? schema.$ref;
82
+ if (typeof refValue !== "string") {
83
+ return anyOrUnknown(refs);
84
+ }
84
85
  const resolved = resolveRef(schema, refValue, refs);
85
86
  if (!resolved) {
86
87
  refs.onUnresolvedRef?.(refValue, refs.path);
@@ -119,6 +120,14 @@ const parseRef = (schema, refs) => {
119
120
  // (or is currently being resolved). This avoids TDZ on true cycles while
120
121
  // letting ordered, acyclic refs stay direct.
121
122
  if (isSameCycle || refs.inProgress.has(refName)) {
123
+ const inObjectProperty = refs.path.includes("properties") ||
124
+ refs.path.includes("patternProperties") ||
125
+ refs.path.includes("additionalProperties");
126
+ if (inObjectProperty && refName === refs.currentSchemaName) {
127
+ // Getter properties defer evaluation, so a direct reference avoids extra lazies
128
+ // for self-recursion.
129
+ return refName;
130
+ }
122
131
  return `z.lazy(() => ${refName})`;
123
132
  }
124
133
  return refName;
@@ -190,7 +199,10 @@ const resolveRef = (schemaNode, ref, refs) => {
190
199
  const loaded = refs.resolveExternalRef(extBase);
191
200
  if (loaded) {
192
201
  // If async resolver is used synchronously here, it will be ignored; keep simple sync for now
193
- const schema = loaded.then ? undefined : loaded;
202
+ const maybePromise = loaded;
203
+ const schema = typeof maybePromise.then === "function"
204
+ ? undefined
205
+ : loaded;
194
206
  if (schema) {
195
207
  const { registry } = buildRefRegistry(schema, extBase);
196
208
  registry.forEach((entry, k) => refs.refRegistry?.set(k, entry));
@@ -338,10 +350,10 @@ const selectParser = (schema, refs) => {
338
350
  return parseNumber(schema);
339
351
  }
340
352
  else if (its.a.primitive(schema, "boolean")) {
341
- return parseBoolean(schema);
353
+ return parseBoolean();
342
354
  }
343
355
  else if (its.a.primitive(schema, "null")) {
344
- return parseNull(schema);
356
+ return parseNull();
345
357
  }
346
358
  else if (its.a.conditional(schema)) {
347
359
  return parseIfThenElse(schema, refs);
@@ -286,7 +286,7 @@ export const parseString = (schema, refs) => {
286
286
  if (contentMediaType != "") {
287
287
  r += contentMediaType;
288
288
  r += withMessage(schema, "contentSchema", ({ value }) => {
289
- if (value && value instanceof Object) {
289
+ if (value && typeof value === "object") {
290
290
  return {
291
291
  opener: `.pipe(${parseSchema(value, refContext)}`,
292
292
  closer: ")",
@@ -1,5 +1,5 @@
1
1
  import { resolveUri } from "./resolveUri.js";
2
- export const buildRefRegistry = (schema, rootBaseUri = "root:///", opts = {}) => {
2
+ export const buildRefRegistry = (schema, rootBaseUri = "root:///") => {
3
3
  const registry = new Map();
4
4
  const walk = (node, baseUri, path) => {
5
5
  if (typeof node !== "object" || node === null)