@gabrielbryk/json-schema-to-zod 2.7.3 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.github/workflows/release.yml +0 -5
  2. package/CHANGELOG.md +17 -0
  3. package/dist/cjs/generators/generateBundle.js +311 -0
  4. package/dist/cjs/index.js +2 -0
  5. package/dist/cjs/jsonSchemaToZod.js +96 -2
  6. package/dist/cjs/parsers/parseArray.js +34 -15
  7. package/dist/cjs/parsers/parseIfThenElse.js +2 -1
  8. package/dist/cjs/parsers/parseNumber.js +81 -39
  9. package/dist/cjs/parsers/parseObject.js +32 -9
  10. package/dist/cjs/parsers/parseSchema.js +23 -1
  11. package/dist/cjs/parsers/parseString.js +294 -54
  12. package/dist/cjs/utils/cycles.js +113 -0
  13. package/dist/cjs/utils/withMessage.js +4 -5
  14. package/dist/esm/Types.js +2 -1
  15. package/dist/esm/cli.js +12 -10
  16. package/dist/esm/generators/generateBundle.js +311 -0
  17. package/dist/esm/index.js +46 -28
  18. package/dist/esm/jsonSchemaToZod.js +105 -7
  19. package/dist/esm/parsers/parseAllOf.js +8 -5
  20. package/dist/esm/parsers/parseAnyOf.js +10 -6
  21. package/dist/esm/parsers/parseArray.js +47 -24
  22. package/dist/esm/parsers/parseBoolean.js +5 -1
  23. package/dist/esm/parsers/parseConst.js +5 -1
  24. package/dist/esm/parsers/parseDefault.js +7 -3
  25. package/dist/esm/parsers/parseEnum.js +5 -1
  26. package/dist/esm/parsers/parseIfThenElse.js +11 -6
  27. package/dist/esm/parsers/parseMultipleType.js +7 -3
  28. package/dist/esm/parsers/parseNot.js +8 -4
  29. package/dist/esm/parsers/parseNull.js +5 -1
  30. package/dist/esm/parsers/parseNullable.js +8 -4
  31. package/dist/esm/parsers/parseNumber.js +88 -42
  32. package/dist/esm/parsers/parseObject.js +59 -33
  33. package/dist/esm/parsers/parseOneOf.js +10 -6
  34. package/dist/esm/parsers/parseSchema.js +85 -59
  35. package/dist/esm/parsers/parseSimpleDiscriminatedOneOf.js +10 -6
  36. package/dist/esm/parsers/parseString.js +303 -59
  37. package/dist/esm/utils/anyOrUnknown.js +5 -1
  38. package/dist/esm/utils/cliTools.js +13 -7
  39. package/dist/esm/utils/cycles.js +113 -0
  40. package/dist/esm/utils/half.js +5 -1
  41. package/dist/esm/utils/jsdocs.js +8 -3
  42. package/dist/esm/utils/omit.js +5 -1
  43. package/dist/esm/utils/withMessage.js +8 -6
  44. package/dist/esm/zodToJsonSchema.js +4 -1
  45. package/dist/types/Types.d.ts +8 -0
  46. package/dist/types/generators/generateBundle.d.ts +57 -0
  47. package/dist/types/index.d.ts +2 -0
  48. package/dist/types/parsers/parseString.d.ts +2 -2
  49. package/dist/types/utils/cycles.d.ts +7 -0
  50. package/dist/types/utils/jsdocs.d.ts +1 -1
  51. package/dist/types/utils/withMessage.d.ts +6 -1
  52. package/package.json +1 -1
@@ -28,9 +28,6 @@ jobs:
28
28
  cache: pnpm
29
29
  registry-url: https://registry.npmjs.org
30
30
 
31
- - name: Update npm (trusted publishing requires >=11.5.1)
32
- run: npm install -g npm@latest
33
-
34
31
  - name: Install dependencies
35
32
  run: pnpm install --frozen-lockfile
36
33
 
@@ -46,5 +43,3 @@ jobs:
46
43
  publish: pnpm changeset publish --access public
47
44
  env:
48
45
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
- NPM_TOKEN: ""
50
- NODE_AUTH_TOKEN: ""
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @gabrielbryk/json-schema-to-zod
2
2
 
3
+ ## 2.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0065ee8: - Add support for JSON Schema `dependentRequired` on objects with optional custom error message.
8
+ - Extend format handling and add bigint format helpers while warning on unknown string formats.
9
+
10
+ ### Patch Changes
11
+
12
+ - 3d57690: - Make $ref handling cycle-aware with SCC-based ordering and minimal z.lazy usage.
13
+ - Add workflow spec fixture to compiled-output tests to guard against TDZ issues.
14
+ - Fix parseString to build a full Refs context when missing, keeping type checks happy.
15
+ - 3d57690: - Switch ESM/typings builds to NodeNext resolution and ensure relative imports include .js extensions for Node ESM compatibility.
16
+ - 82aa953: Fix patternProperties validation under Zod v4 by preserving regex patterns and handling missing `ctx.path`.
17
+ - a501e7d: Adjust release workflow to rely on the default npm from setup-node and drop unused tokens.
18
+ - 43f2abc: Update object record generation to use `z.record(z.string(), …)` for Zod v4 compatibility.
19
+
3
20
  ## 2.7.3
4
21
 
5
22
  ### Patch Changes
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateSchemaBundle = void 0;
4
+ const jsonSchemaToZod_js_1 = require("../jsonSchemaToZod.js");
5
+ const generateSchemaBundle = (schema, options = {}) => {
6
+ const module = options.module ?? "esm";
7
+ const rootName = options.splitDefs?.rootName ?? options.name ?? "RootSchema";
8
+ const rootTypeName = typeof options.type === "string"
9
+ ? options.type
10
+ : options.splitDefs?.rootTypeName ?? (typeof options.type === "boolean" && options.type ? rootName : undefined);
11
+ if (!schema || typeof schema !== "object") {
12
+ throw new Error("generateSchemaBundle requires an object schema");
13
+ }
14
+ const defs = schema.$defs || schema.definitions || {};
15
+ const defNames = Object.keys(defs);
16
+ const defInfoMap = buildDefInfoMap(defNames, defs, options);
17
+ const cycles = detectCycles(defInfoMap);
18
+ for (const defName of cycles) {
19
+ const info = defInfoMap.get(defName);
20
+ if (info)
21
+ info.hasCycle = true;
22
+ }
23
+ const files = [];
24
+ // Generate individual $def files
25
+ for (const defName of defNames) {
26
+ const info = defInfoMap.get(defName);
27
+ const usedRefs = new Set();
28
+ const defSchemaWithDefs = {
29
+ ...defs[defName],
30
+ $defs: defs,
31
+ };
32
+ const zodSchema = (0, jsonSchemaToZod_js_1.jsonSchemaToZod)(defSchemaWithDefs, {
33
+ ...options,
34
+ module,
35
+ name: info.schemaName,
36
+ type: info.typeName,
37
+ parserOverride: createRefHandler(defName, defInfoMap, usedRefs, defs, options),
38
+ });
39
+ const finalSchema = buildSchemaFile(zodSchema, usedRefs, defInfoMap, module);
40
+ const fileName = options.splitDefs?.fileName?.(defName, { isRoot: false }) ?? `${defName}.schema.ts`;
41
+ files.push({ fileName, contents: finalSchema });
42
+ }
43
+ // Generate root schema if requested
44
+ if (options.splitDefs?.includeRoot ?? true) {
45
+ const usedRefs = new Set();
46
+ const workflowSchemaWithDefs = {
47
+ ...schema,
48
+ };
49
+ const workflowZodSchema = (0, jsonSchemaToZod_js_1.jsonSchemaToZod)(workflowSchemaWithDefs, {
50
+ ...options,
51
+ module,
52
+ name: rootName,
53
+ type: rootTypeName,
54
+ parserOverride: createRefHandler(null, defInfoMap, usedRefs, defs, options),
55
+ });
56
+ const finalWorkflow = buildSchemaFile(workflowZodSchema, usedRefs, defInfoMap, module);
57
+ const rootFile = options.splitDefs?.fileName?.("root", { isRoot: true }) ?? "workflow.schema.ts";
58
+ files.push({ fileName: rootFile, contents: finalWorkflow });
59
+ }
60
+ // Nested types extraction (optional)
61
+ const nestedTypesEnabled = options.nestedTypes?.enable;
62
+ if (nestedTypesEnabled) {
63
+ const nestedTypes = collectNestedTypes(schema, defs, defNames, rootTypeName ?? rootName);
64
+ if (nestedTypes.length > 0) {
65
+ const nestedFileName = options.nestedTypes?.fileName ?? "nested-types.ts";
66
+ const nestedContent = generateNestedTypesFile(nestedTypes);
67
+ files.push({ fileName: nestedFileName, contents: nestedContent });
68
+ }
69
+ }
70
+ return { files, defNames };
71
+ };
72
+ exports.generateSchemaBundle = generateSchemaBundle;
73
+ // ---------------------------------------------------------------------------
74
+ // Internals
75
+ // ---------------------------------------------------------------------------
76
+ const toPascalCase = (str) => str
77
+ .split(/[-_]/)
78
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
79
+ .join("");
80
+ const buildDefInfoMap = (defNames, defs, options) => {
81
+ const map = new Map();
82
+ for (const defName of defNames) {
83
+ const dependencies = findRefDependencies(defs[defName], defNames);
84
+ const pascalName = toPascalCase(defName);
85
+ const schemaName = options.splitDefs?.schemaName?.(defName, { isRoot: false }) ?? `${pascalName}Schema`;
86
+ const typeName = options.splitDefs?.typeName?.(defName, { isRoot: false }) ?? pascalName;
87
+ map.set(defName, {
88
+ name: defName,
89
+ pascalName,
90
+ schemaName,
91
+ typeName,
92
+ dependencies,
93
+ hasCycle: false,
94
+ });
95
+ }
96
+ return map;
97
+ };
98
+ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options) => {
99
+ return (schema, refs) => {
100
+ if (typeof schema["$ref"] === "string") {
101
+ const refPath = schema["$ref"];
102
+ const match = refPath.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
103
+ if (match) {
104
+ const refName = match[1];
105
+ const refInfo = defInfoMap.get(refName);
106
+ if (refInfo) {
107
+ // Track imports when referencing other defs
108
+ if (refName !== currentDefName) {
109
+ usedRefs.add(refName);
110
+ }
111
+ const isCycle = refName === currentDefName || (refInfo.hasCycle && !!currentDefName);
112
+ const resolved = options.refResolution?.onRef?.({
113
+ ref: refPath,
114
+ refName,
115
+ currentDef: currentDefName,
116
+ path: refs.path,
117
+ isCycle,
118
+ });
119
+ if (resolved)
120
+ return resolved;
121
+ return refInfo.schemaName;
122
+ }
123
+ }
124
+ const unknown = options.refResolution?.onUnknownRef?.({ ref: refPath, currentDef: currentDefName });
125
+ if (unknown)
126
+ return unknown;
127
+ return options.useUnknown ? "z.unknown()" : "z.any()";
128
+ }
129
+ // Inline $defs within a schema
130
+ if (schema["$defs"] && typeof schema["$defs"] === "object") {
131
+ return (0, jsonSchemaToZod_js_1.jsonSchemaToZod)(schema, {
132
+ ...options,
133
+ module: options.module ?? "esm",
134
+ parserOverride: createRefHandler(currentDefName, defInfoMap, usedRefs, allDefs, options),
135
+ });
136
+ }
137
+ return undefined;
138
+ };
139
+ };
140
+ const buildSchemaFile = (zodCode, usedRefs, defInfoMap, module) => {
141
+ if (module !== "esm")
142
+ return zodCode;
143
+ const imports = [];
144
+ for (const refName of [...usedRefs].sort()) {
145
+ const refInfo = defInfoMap.get(refName);
146
+ if (refInfo) {
147
+ imports.push(`import { ${refInfo.schemaName} } from './${refName}.schema.js';`);
148
+ }
149
+ }
150
+ if (!imports.length)
151
+ return zodCode;
152
+ return zodCode.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`);
153
+ };
154
+ const findRefDependencies = (schema, validDefNames) => {
155
+ const deps = new Set();
156
+ function traverse(obj) {
157
+ if (obj === null || typeof obj !== "object")
158
+ return;
159
+ if (Array.isArray(obj)) {
160
+ obj.forEach(traverse);
161
+ return;
162
+ }
163
+ const record = obj;
164
+ if (typeof record["$ref"] === "string") {
165
+ const ref = record["$ref"];
166
+ const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
167
+ if (match && validDefNames.includes(match[1])) {
168
+ deps.add(match[1]);
169
+ }
170
+ }
171
+ for (const value of Object.values(record)) {
172
+ traverse(value);
173
+ }
174
+ }
175
+ traverse(schema);
176
+ return deps;
177
+ };
178
+ const detectCycles = (defInfoMap) => {
179
+ const cycleNodes = new Set();
180
+ const visited = new Set();
181
+ const recursionStack = new Set();
182
+ function dfs(node, path) {
183
+ if (recursionStack.has(node)) {
184
+ const cycleStart = path.indexOf(node);
185
+ for (let i = cycleStart; i < path.length; i++) {
186
+ cycleNodes.add(path[i]);
187
+ }
188
+ cycleNodes.add(node);
189
+ return true;
190
+ }
191
+ if (visited.has(node))
192
+ return false;
193
+ visited.add(node);
194
+ recursionStack.add(node);
195
+ const info = defInfoMap.get(node);
196
+ if (info) {
197
+ for (const dep of info.dependencies) {
198
+ dfs(dep, [...path, node]);
199
+ }
200
+ }
201
+ recursionStack.delete(node);
202
+ return false;
203
+ }
204
+ for (const defName of defInfoMap.keys()) {
205
+ if (!visited.has(defName)) {
206
+ dfs(defName, []);
207
+ }
208
+ }
209
+ return cycleNodes;
210
+ };
211
+ const collectNestedTypes = (rootSchema, defs, defNames, rootTypeName) => {
212
+ const allNestedTypes = [];
213
+ for (const defName of defNames) {
214
+ const defSchema = defs[defName];
215
+ const parentTypeName = toPascalCase(defName);
216
+ const nestedTypes = findNestedTypesInSchema(defSchema, parentTypeName, defNames);
217
+ for (const nested of nestedTypes) {
218
+ nested.file = defName;
219
+ nested.parentType = parentTypeName;
220
+ allNestedTypes.push(nested);
221
+ }
222
+ }
223
+ const workflowNestedTypes = findNestedTypesInSchema({ properties: rootSchema.properties, required: rootSchema.required }, rootTypeName, defNames);
224
+ for (const nested of workflowNestedTypes) {
225
+ nested.file = "workflow";
226
+ nested.parentType = rootTypeName;
227
+ allNestedTypes.push(nested);
228
+ }
229
+ const uniqueNestedTypes = new Map();
230
+ for (const nested of allNestedTypes) {
231
+ if (!uniqueNestedTypes.has(nested.typeName) && nested.propertyPath.length > 0) {
232
+ uniqueNestedTypes.set(nested.typeName, nested);
233
+ }
234
+ }
235
+ return [...uniqueNestedTypes.values()];
236
+ };
237
+ const findNestedTypesInSchema = (schema, parentTypeName, defNames, currentPath = []) => {
238
+ const nestedTypes = [];
239
+ if (schema === null || typeof schema !== "object")
240
+ return nestedTypes;
241
+ const record = schema;
242
+ if (record.title && typeof record.title === "string" && record.type === "object") {
243
+ const title = record.title;
244
+ if (title !== parentTypeName && !defNames.map((d) => toPascalCase(d)).includes(title)) {
245
+ nestedTypes.push({
246
+ typeName: title,
247
+ parentType: parentTypeName,
248
+ propertyPath: [...currentPath],
249
+ file: "",
250
+ });
251
+ }
252
+ }
253
+ if (record.properties && typeof record.properties === "object") {
254
+ for (const [propName, propSchema] of Object.entries(record.properties)) {
255
+ nestedTypes.push(...findNestedTypesInSchema(propSchema, parentTypeName, defNames, [...currentPath, propName]));
256
+ }
257
+ }
258
+ if (Array.isArray(record.allOf)) {
259
+ for (const item of record.allOf) {
260
+ nestedTypes.push(...findNestedTypesInSchema(item, parentTypeName, defNames, currentPath));
261
+ }
262
+ }
263
+ if (record.items) {
264
+ nestedTypes.push(...findNestedTypesInSchema(record.items, parentTypeName, defNames, currentPath));
265
+ }
266
+ if (record.additionalProperties && typeof record.additionalProperties === "object") {
267
+ nestedTypes.push(...findNestedTypesInSchema(record.additionalProperties, parentTypeName, defNames, currentPath));
268
+ }
269
+ return nestedTypes;
270
+ };
271
+ const generateNestedTypesFile = (nestedTypes) => {
272
+ const lines = [
273
+ "/**",
274
+ " * Auto-generated nested type exports",
275
+ " * ",
276
+ " * These types are inline within parent schemas but commonly needed separately.",
277
+ " * They are extracted using TypeScript indexed access types.",
278
+ " */",
279
+ "",
280
+ ];
281
+ const byParent = new Map();
282
+ for (const info of nestedTypes) {
283
+ if (!byParent.has(info.parentType)) {
284
+ byParent.set(info.parentType, []);
285
+ }
286
+ byParent.get(info.parentType).push(info);
287
+ }
288
+ const imports = new Set();
289
+ for (const info of nestedTypes) {
290
+ imports.add(info.file);
291
+ }
292
+ for (const file of [...imports].sort()) {
293
+ const typeName = file === "workflow" ? "Workflow" : toPascalCase(file);
294
+ lines.push(`import type { ${typeName} } from './${file}.schema.js';`);
295
+ }
296
+ lines.push("");
297
+ for (const [parentType, types] of [...byParent.entries()].sort()) {
298
+ lines.push(`// From ${parentType}`);
299
+ for (const info of types.sort((a, b) => a.typeName.localeCompare(b.typeName))) {
300
+ if (info.propertyPath.length > 0) {
301
+ let accessExpr = parentType;
302
+ for (const prop of info.propertyPath) {
303
+ accessExpr = `NonNullable<${accessExpr}['${prop}']>`;
304
+ }
305
+ lines.push(`export type ${info.typeName} = ${accessExpr};`);
306
+ }
307
+ }
308
+ lines.push("");
309
+ }
310
+ return lines.join("\n");
311
+ };
package/dist/cjs/index.js CHANGED
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./Types.js"), exports);
18
+ __exportStar(require("./generators/generateBundle.js"), exports);
18
19
  __exportStar(require("./jsonSchemaToZod.js"), exports);
19
20
  __exportStar(require("./parsers/parseAllOf.js"), exports);
20
21
  __exportStar(require("./parsers/parseAnyOf.js"), exports);
@@ -35,6 +36,7 @@ __exportStar(require("./parsers/parseSchema.js"), exports);
35
36
  __exportStar(require("./parsers/parseSimpleDiscriminatedOneOf.js"), exports);
36
37
  __exportStar(require("./parsers/parseString.js"), exports);
37
38
  __exportStar(require("./utils/anyOrUnknown.js"), exports);
39
+ __exportStar(require("./utils/cycles.js"), exports);
38
40
  __exportStar(require("./utils/half.js"), exports);
39
41
  __exportStar(require("./utils/jsdocs.js"), exports);
40
42
  __exportStar(require("./utils/omit.js"), exports);
@@ -3,33 +3,59 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.jsonSchemaToZod = void 0;
4
4
  const parseSchema_js_1 = require("./parsers/parseSchema.js");
5
5
  const jsdocs_js_1 = require("./utils/jsdocs.js");
6
+ const cycles_js_1 = require("./utils/cycles.js");
6
7
  const jsonSchemaToZod = (schema, { module, name, type, noImport, ...rest } = {}) => {
7
8
  if (type && (!name || module !== "esm")) {
8
9
  throw new Error("Option `type` requires `name` to be set and `module` to be `esm`");
9
10
  }
10
- const declarations = new Map();
11
11
  const refNameByPointer = new Map();
12
12
  const usedNames = new Set();
13
13
  const exportRefs = rest.exportRefs ?? true;
14
14
  const withMeta = rest.withMeta ?? true;
15
15
  if (name)
16
16
  usedNames.add(name);
17
+ // Pass 1: collect declarations/dependencies to detect cycles before emitting
18
+ const pass1 = {
19
+ module,
20
+ name,
21
+ path: [],
22
+ seen: new Map(),
23
+ declarations: new Map(),
24
+ dependencies: new Map(),
25
+ inProgress: new Set(),
26
+ refNameByPointer,
27
+ usedNames,
28
+ root: schema,
29
+ currentSchemaName: name,
30
+ ...rest,
31
+ withMeta,
32
+ };
33
+ (0, parseSchema_js_1.parseSchema)(schema, pass1);
34
+ const names = Array.from(pass1.declarations.keys());
35
+ const cycleRefNames = (0, cycles_js_1.detectCycles)(names, pass1.dependencies);
36
+ const { componentByName } = (0, cycles_js_1.computeScc)(names, pass1.dependencies);
37
+ // Pass 2: generate with cycle awareness
38
+ const declarations = new Map();
39
+ const dependencies = new Map();
17
40
  const parsedSchema = (0, parseSchema_js_1.parseSchema)(schema, {
18
41
  module,
19
42
  name,
20
43
  path: [],
21
44
  seen: new Map(),
22
45
  declarations,
46
+ dependencies,
23
47
  inProgress: new Set(),
24
48
  refNameByPointer,
25
49
  usedNames,
26
50
  root: schema,
27
51
  currentSchemaName: name,
52
+ cycleRefNames,
53
+ cycleComponentByName: componentByName,
28
54
  ...rest,
29
55
  withMeta,
30
56
  });
31
57
  const declarationBlock = declarations.size
32
- ? Array.from(declarations.entries())
58
+ ? orderDeclarations(Array.from(declarations.entries()), dependencies)
33
59
  .map(([refName, value]) => {
34
60
  const shouldExport = exportRefs && module === "esm";
35
61
  const decl = `${shouldExport ? "export " : ""}const ${refName} = ${value}`;
@@ -76,3 +102,71 @@ const jsonSchemaToZod = (schema, { module, name, type, noImport, ...rest } = {})
76
102
  return `${combined}${shouldEndWithNewline ? "\n" : ""}`;
77
103
  };
78
104
  exports.jsonSchemaToZod = jsonSchemaToZod;
105
+ function orderDeclarations(entries, dependencies) {
106
+ const valueByName = new Map(entries);
107
+ const depGraph = new Map();
108
+ // Seed with explicit dependencies recorded during parsing
109
+ for (const [from, set] of dependencies.entries()) {
110
+ const onlyKnown = new Set();
111
+ for (const dep of set) {
112
+ if (valueByName.has(dep) && dep !== from) {
113
+ onlyKnown.add(dep);
114
+ }
115
+ }
116
+ if (onlyKnown.size)
117
+ depGraph.set(from, onlyKnown);
118
+ }
119
+ // Also infer deps by scanning declaration bodies for referenced names
120
+ const names = Array.from(valueByName.keys());
121
+ for (const [name, value] of entries) {
122
+ const deps = depGraph.get(name) ?? new Set();
123
+ for (const candidate of names) {
124
+ if (candidate === name)
125
+ continue;
126
+ const matcher = new RegExp(`\\b${candidate}\\b`);
127
+ if (matcher.test(value)) {
128
+ deps.add(candidate);
129
+ }
130
+ }
131
+ if (deps.size)
132
+ depGraph.set(name, deps);
133
+ }
134
+ const ordered = [];
135
+ const perm = new Set();
136
+ const temp = new Set();
137
+ const visit = (name) => {
138
+ if (perm.has(name))
139
+ return;
140
+ if (temp.has(name)) {
141
+ // Cycle detected; break it but still include the node.
142
+ temp.delete(name);
143
+ perm.add(name);
144
+ ordered.push(name);
145
+ return;
146
+ }
147
+ temp.add(name);
148
+ const deps = depGraph.get(name);
149
+ if (deps) {
150
+ for (const dep of deps) {
151
+ if (valueByName.has(dep)) {
152
+ visit(dep);
153
+ }
154
+ }
155
+ }
156
+ temp.delete(name);
157
+ perm.add(name);
158
+ ordered.push(name);
159
+ };
160
+ for (const name of valueByName.keys()) {
161
+ visit(name);
162
+ }
163
+ const unique = [];
164
+ const seen = new Set();
165
+ for (const name of ordered) {
166
+ if (!seen.has(name)) {
167
+ seen.add(name);
168
+ unique.push(name);
169
+ }
170
+ }
171
+ return unique.map((name) => [name, valueByName.get(name)]);
172
+ }
@@ -32,22 +32,41 @@ const parseArray = (schema, refs) => {
32
32
  ...refs,
33
33
  path: [...refs.path, "items"],
34
34
  })})`;
35
- r += (0, withMessage_js_1.withMessage)(schema, "minItems", ({ json }) => [
36
- `.min(${json}`,
37
- ", ",
38
- ")",
39
- ]);
40
- r += (0, withMessage_js_1.withMessage)(schema, "maxItems", ({ json }) => [
41
- `.max(${json}`,
42
- ", ",
43
- ")",
44
- ]);
35
+ r += (0, withMessage_js_1.withMessage)(schema, "minItems", ({ json }) => ({
36
+ opener: `.min(${json}`,
37
+ closer: ")",
38
+ messagePrefix: ", { error: ",
39
+ messageCloser: " })",
40
+ }));
41
+ r += (0, withMessage_js_1.withMessage)(schema, "maxItems", ({ json }) => ({
42
+ opener: `.max(${json}`,
43
+ closer: ")",
44
+ messagePrefix: ", { error: ",
45
+ messageCloser: " })",
46
+ }));
45
47
  if (schema.uniqueItems === true) {
46
- r += (0, withMessage_js_1.withMessage)(schema, "uniqueItems", () => [
47
- ".unique(",
48
- "",
49
- ")",
50
- ]);
48
+ r += `.superRefine((arr, ctx) => {
49
+ const seen = new Set();
50
+ for (const [index, value] of arr.entries()) {
51
+ let key;
52
+ if (value && typeof value === "object") {
53
+ try {
54
+ key = JSON.stringify(value);
55
+ } catch {
56
+ key = String(value);
57
+ }
58
+ } else {
59
+ key = JSON.stringify(value);
60
+ }
61
+
62
+ if (seen.has(key)) {
63
+ ctx.addIssue({ code: "custom", message: "Array items must be unique", path: [index] });
64
+ return;
65
+ }
66
+
67
+ seen.add(key);
68
+ }
69
+ })`;
51
70
  }
52
71
  if (schema.contains) {
53
72
  const containsSchema = (0, parseSchema_js_1.parseSchema)(schema.contains, {
@@ -17,7 +17,8 @@ const parseIfThenElse = (schema, refs) => {
17
17
  ? ${$then}.safeParse(value)
18
18
  : ${$else}.safeParse(value);
19
19
  if (!result.success) {
20
- result.error.errors.forEach((error) => ctx.addIssue(error))
20
+ const issues = result.error.issues ?? result.error.errors ?? [];
21
+ issues.forEach((issue) => ctx.addIssue(issue))
21
22
  }
22
23
  })`;
23
24
  // Store original if/then/else for JSON Schema round-trip