zod 4.2.0-canary.20251202T062120 → 4.2.0-canary.20251213T203150

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/v4/classic/schemas.ts +97 -5
  3. package/src/v4/classic/tests/fix-json-issue.test.ts +26 -0
  4. package/src/v4/classic/tests/json.test.ts +4 -3
  5. package/src/v4/classic/tests/standard-schema.test.ts +77 -0
  6. package/src/v4/classic/tests/to-json-schema-methods.test.ts +438 -0
  7. package/src/v4/classic/tests/to-json-schema.test.ts +66 -30
  8. package/src/v4/core/index.ts +2 -0
  9. package/src/v4/core/json-schema-generator.ts +124 -0
  10. package/src/v4/core/json-schema-processors.ts +630 -0
  11. package/src/v4/core/schemas.ts +8 -13
  12. package/src/v4/core/standard-schema.ts +114 -19
  13. package/src/v4/core/to-json-schema.ts +373 -827
  14. package/src/v4/mini/schemas.ts +2 -2
  15. package/src/v4/mini/tests/standard-schema.test.ts +17 -0
  16. package/v4/classic/schemas.cjs +48 -0
  17. package/v4/classic/schemas.d.cts +35 -0
  18. package/v4/classic/schemas.d.ts +35 -0
  19. package/v4/classic/schemas.js +48 -0
  20. package/v4/core/index.cjs +5 -1
  21. package/v4/core/index.d.cts +2 -0
  22. package/v4/core/index.d.ts +2 -0
  23. package/v4/core/index.js +2 -0
  24. package/v4/core/json-schema-generator.cjs +99 -0
  25. package/v4/core/json-schema-generator.d.cts +64 -0
  26. package/v4/core/json-schema-generator.d.ts +64 -0
  27. package/v4/core/json-schema-generator.js +95 -0
  28. package/v4/core/json-schema-processors.cjs +617 -0
  29. package/v4/core/json-schema-processors.d.cts +49 -0
  30. package/v4/core/json-schema-processors.d.ts +49 -0
  31. package/v4/core/json-schema-processors.js +574 -0
  32. package/v4/core/schemas.cjs +0 -10
  33. package/v4/core/schemas.d.cts +4 -1
  34. package/v4/core/schemas.d.ts +4 -1
  35. package/v4/core/schemas.js +0 -10
  36. package/v4/core/standard-schema.d.cts +90 -19
  37. package/v4/core/standard-schema.d.ts +90 -19
  38. package/v4/core/to-json-schema.cjs +302 -793
  39. package/v4/core/to-json-schema.d.cts +56 -33
  40. package/v4/core/to-json-schema.d.ts +56 -33
  41. package/v4/core/to-json-schema.js +296 -791
  42. package/v4/mini/schemas.d.cts +2 -2
  43. package/v4/mini/schemas.d.ts +2 -2
@@ -1,823 +1,311 @@
1
- import { $ZodRegistry, globalRegistry } from "./registries.js";
2
- import { getEnumValues } from "./util.js";
3
- export class JSONSchemaGenerator {
4
- constructor(params) {
5
- this.counter = 0;
6
- this.metadataRegistry = params?.metadata ?? globalRegistry;
7
- this.target = params?.target ?? "draft-2020-12";
8
- this.unrepresentable = params?.unrepresentable ?? "throw";
9
- this.override = params?.override ?? (() => { });
10
- this.io = params?.io ?? "output";
11
- this.seen = new Map();
1
+ import { globalRegistry } from "./registries.js";
2
+ // function initializeContext<T extends schemas.$ZodType>(inputs: JSONSchemaGeneratorParams<T>): ToJSONSchemaContext<T> {
3
+ // return {
4
+ // processor: inputs.processor,
5
+ // metadataRegistry: inputs.metadata ?? globalRegistry,
6
+ // target: inputs.target ?? "draft-2020-12",
7
+ // unrepresentable: inputs.unrepresentable ?? "throw",
8
+ // };
9
+ // }
10
+ export function initializeContext(params) {
11
+ // Normalize target: convert old non-hyphenated versions to hyphenated versions
12
+ let target = params?.target ?? "draft-2020-12";
13
+ if (target === "draft-4")
14
+ target = "draft-04";
15
+ if (target === "draft-7")
16
+ target = "draft-07";
17
+ return {
18
+ processors: params.processors ?? {},
19
+ metadataRegistry: params?.metadata ?? globalRegistry,
20
+ target,
21
+ unrepresentable: params?.unrepresentable ?? "throw",
22
+ override: params?.override ?? (() => { }),
23
+ io: params?.io ?? "output",
24
+ counter: 0,
25
+ seen: new Map(),
26
+ cycles: params?.cycles ?? "ref",
27
+ reused: params?.reused ?? "inline",
28
+ external: params?.external ?? undefined,
29
+ };
30
+ }
31
+ export function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
32
+ var _a;
33
+ const def = schema._zod.def;
34
+ // check for schema in seens
35
+ const seen = ctx.seen.get(schema);
36
+ if (seen) {
37
+ seen.count++;
38
+ // check if cycle
39
+ const isCycle = _params.schemaPath.includes(schema);
40
+ if (isCycle) {
41
+ seen.cycle = _params.path;
42
+ }
43
+ return seen.schema;
44
+ }
45
+ // initialize
46
+ const result = { schema: {}, count: 1, cycle: undefined, path: _params.path };
47
+ ctx.seen.set(schema, result);
48
+ // custom method overrides default behavior
49
+ const overrideSchema = schema._zod.toJSONSchema?.();
50
+ if (overrideSchema) {
51
+ result.schema = overrideSchema;
12
52
  }
13
- process(schema, _params = { path: [], schemaPath: [] }) {
14
- var _a;
15
- const def = schema._zod.def;
16
- const formatMap = {
17
- guid: "uuid",
18
- url: "uri",
19
- datetime: "date-time",
20
- json_string: "json-string",
21
- regex: "", // do not set
53
+ else {
54
+ const params = {
55
+ ..._params,
56
+ schemaPath: [..._params.schemaPath, schema],
57
+ path: _params.path,
22
58
  };
23
- // check for schema in seens
24
- const seen = this.seen.get(schema);
25
- if (seen) {
26
- seen.count++;
27
- // check if cycle
28
- const isCycle = _params.schemaPath.includes(schema);
29
- if (isCycle) {
30
- seen.cycle = _params.path;
31
- }
32
- return seen.schema;
59
+ const parent = schema._zod.parent;
60
+ if (parent) {
61
+ // schema was cloned from another schema
62
+ result.ref = parent;
63
+ process(parent, ctx, params);
64
+ ctx.seen.get(parent).isParent = true;
33
65
  }
34
- // initialize
35
- const result = { schema: {}, count: 1, cycle: undefined, path: _params.path };
36
- this.seen.set(schema, result);
37
- // custom method overrides default behavior
38
- const overrideSchema = schema._zod.toJSONSchema?.();
39
- if (overrideSchema) {
40
- result.schema = overrideSchema;
66
+ else if (schema._zod.processJSONSchema) {
67
+ schema._zod.processJSONSchema(ctx, result.schema, params);
41
68
  }
42
69
  else {
43
- const params = {
44
- ..._params,
45
- schemaPath: [..._params.schemaPath, schema],
46
- path: _params.path,
47
- };
48
- const parent = schema._zod.parent;
49
- if (parent) {
50
- // schema was cloned from another schema
51
- result.ref = parent;
52
- this.process(parent, params);
53
- this.seen.get(parent).isParent = true;
54
- }
55
- else {
56
- const _json = result.schema;
57
- switch (def.type) {
58
- case "string": {
59
- const json = _json;
60
- json.type = "string";
61
- const { minimum, maximum, format, patterns, contentEncoding } = schema._zod
62
- .bag;
63
- if (typeof minimum === "number")
64
- json.minLength = minimum;
65
- if (typeof maximum === "number")
66
- json.maxLength = maximum;
67
- // custom pattern overrides format
68
- if (format) {
69
- json.format = formatMap[format] ?? format;
70
- if (json.format === "")
71
- delete json.format; // empty format is not valid
72
- }
73
- if (contentEncoding)
74
- json.contentEncoding = contentEncoding;
75
- if (patterns && patterns.size > 0) {
76
- const regexes = [...patterns];
77
- if (regexes.length === 1)
78
- json.pattern = regexes[0].source;
79
- else if (regexes.length > 1) {
80
- result.schema.allOf = [
81
- ...regexes.map((regex) => ({
82
- ...(this.target === "draft-7" || this.target === "draft-4" || this.target === "openapi-3.0"
83
- ? { type: "string" }
84
- : {}),
85
- pattern: regex.source,
86
- })),
87
- ];
88
- }
89
- }
90
- break;
91
- }
92
- case "number": {
93
- const json = _json;
94
- const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
95
- if (typeof format === "string" && format.includes("int"))
96
- json.type = "integer";
97
- else
98
- json.type = "number";
99
- if (typeof exclusiveMinimum === "number") {
100
- if (this.target === "draft-4" || this.target === "openapi-3.0") {
101
- json.minimum = exclusiveMinimum;
102
- json.exclusiveMinimum = true;
103
- }
104
- else {
105
- json.exclusiveMinimum = exclusiveMinimum;
106
- }
107
- }
108
- if (typeof minimum === "number") {
109
- json.minimum = minimum;
110
- if (typeof exclusiveMinimum === "number" && this.target !== "draft-4") {
111
- if (exclusiveMinimum >= minimum)
112
- delete json.minimum;
113
- else
114
- delete json.exclusiveMinimum;
115
- }
116
- }
117
- if (typeof exclusiveMaximum === "number") {
118
- if (this.target === "draft-4" || this.target === "openapi-3.0") {
119
- json.maximum = exclusiveMaximum;
120
- json.exclusiveMaximum = true;
121
- }
122
- else {
123
- json.exclusiveMaximum = exclusiveMaximum;
124
- }
125
- }
126
- if (typeof maximum === "number") {
127
- json.maximum = maximum;
128
- if (typeof exclusiveMaximum === "number" && this.target !== "draft-4") {
129
- if (exclusiveMaximum <= maximum)
130
- delete json.maximum;
131
- else
132
- delete json.exclusiveMaximum;
133
- }
134
- }
135
- if (typeof multipleOf === "number")
136
- json.multipleOf = multipleOf;
137
- break;
138
- }
139
- case "boolean": {
140
- const json = _json;
141
- json.type = "boolean";
142
- break;
143
- }
144
- case "bigint": {
145
- if (this.unrepresentable === "throw") {
146
- throw new Error("BigInt cannot be represented in JSON Schema");
147
- }
148
- break;
149
- }
150
- case "symbol": {
151
- if (this.unrepresentable === "throw") {
152
- throw new Error("Symbols cannot be represented in JSON Schema");
153
- }
154
- break;
155
- }
156
- case "null": {
157
- if (this.target === "openapi-3.0") {
158
- _json.type = "string";
159
- _json.nullable = true;
160
- _json.enum = [null];
161
- }
162
- else
163
- _json.type = "null";
164
- break;
165
- }
166
- case "any": {
167
- break;
168
- }
169
- case "unknown": {
170
- break;
171
- }
172
- case "undefined": {
173
- if (this.unrepresentable === "throw") {
174
- throw new Error("Undefined cannot be represented in JSON Schema");
175
- }
176
- break;
177
- }
178
- case "void": {
179
- if (this.unrepresentable === "throw") {
180
- throw new Error("Void cannot be represented in JSON Schema");
181
- }
182
- break;
183
- }
184
- case "never": {
185
- _json.not = {};
186
- break;
187
- }
188
- case "date": {
189
- if (this.unrepresentable === "throw") {
190
- throw new Error("Date cannot be represented in JSON Schema");
191
- }
192
- break;
193
- }
194
- case "array": {
195
- const json = _json;
196
- const { minimum, maximum } = schema._zod.bag;
197
- if (typeof minimum === "number")
198
- json.minItems = minimum;
199
- if (typeof maximum === "number")
200
- json.maxItems = maximum;
201
- json.type = "array";
202
- json.items = this.process(def.element, { ...params, path: [...params.path, "items"] });
203
- break;
204
- }
205
- case "object": {
206
- const json = _json;
207
- json.type = "object";
208
- json.properties = {};
209
- const shape = def.shape; // params.shapeCache.get(schema)!;
210
- for (const key in shape) {
211
- json.properties[key] = this.process(shape[key], {
212
- ...params,
213
- path: [...params.path, "properties", key],
214
- });
215
- }
216
- // required keys
217
- const allKeys = new Set(Object.keys(shape));
218
- // const optionalKeys = new Set(def.optional);
219
- const requiredKeys = new Set([...allKeys].filter((key) => {
220
- const v = def.shape[key]._zod;
221
- if (this.io === "input") {
222
- return v.optin === undefined;
223
- }
224
- else {
225
- return v.optout === undefined;
226
- }
227
- }));
228
- if (requiredKeys.size > 0) {
229
- json.required = Array.from(requiredKeys);
230
- }
231
- // catchall
232
- if (def.catchall?._zod.def.type === "never") {
233
- // strict
234
- json.additionalProperties = false;
235
- }
236
- else if (!def.catchall) {
237
- // regular
238
- if (this.io === "output")
239
- json.additionalProperties = false;
240
- }
241
- else if (def.catchall) {
242
- json.additionalProperties = this.process(def.catchall, {
243
- ...params,
244
- path: [...params.path, "additionalProperties"],
245
- });
246
- }
247
- break;
248
- }
249
- case "union": {
250
- const json = _json;
251
- // Discriminated unions use oneOf (exactly one match) instead of anyOf (one or more matches)
252
- // because the discriminator field ensures mutual exclusivity between options in JSON Schema
253
- const isDiscriminated = def.discriminator !== undefined;
254
- const options = def.options.map((x, i) => this.process(x, {
255
- ...params,
256
- path: [...params.path, isDiscriminated ? "oneOf" : "anyOf", i],
257
- }));
258
- if (isDiscriminated) {
259
- json.oneOf = options;
260
- }
261
- else {
262
- json.anyOf = options;
263
- }
264
- break;
265
- }
266
- case "intersection": {
267
- const json = _json;
268
- const a = this.process(def.left, {
269
- ...params,
270
- path: [...params.path, "allOf", 0],
271
- });
272
- const b = this.process(def.right, {
273
- ...params,
274
- path: [...params.path, "allOf", 1],
275
- });
276
- const isSimpleIntersection = (val) => "allOf" in val && Object.keys(val).length === 1;
277
- const allOf = [
278
- ...(isSimpleIntersection(a) ? a.allOf : [a]),
279
- ...(isSimpleIntersection(b) ? b.allOf : [b]),
280
- ];
281
- json.allOf = allOf;
282
- break;
283
- }
284
- case "tuple": {
285
- const json = _json;
286
- json.type = "array";
287
- const prefixPath = this.target === "draft-2020-12" ? "prefixItems" : "items";
288
- const restPath = this.target === "draft-2020-12" ? "items" : this.target === "openapi-3.0" ? "items" : "additionalItems";
289
- const prefixItems = def.items.map((x, i) => this.process(x, {
290
- ...params,
291
- path: [...params.path, prefixPath, i],
292
- }));
293
- const rest = def.rest
294
- ? this.process(def.rest, {
295
- ...params,
296
- path: [...params.path, restPath, ...(this.target === "openapi-3.0" ? [def.items.length] : [])],
297
- })
298
- : null;
299
- if (this.target === "draft-2020-12") {
300
- json.prefixItems = prefixItems;
301
- if (rest) {
302
- json.items = rest;
303
- }
304
- }
305
- else if (this.target === "openapi-3.0") {
306
- json.items = {
307
- anyOf: prefixItems,
308
- };
309
- if (rest) {
310
- json.items.anyOf.push(rest);
311
- }
312
- json.minItems = prefixItems.length;
313
- if (!rest) {
314
- json.maxItems = prefixItems.length;
315
- }
316
- }
317
- else {
318
- json.items = prefixItems;
319
- if (rest) {
320
- json.additionalItems = rest;
321
- }
322
- }
323
- // length
324
- const { minimum, maximum } = schema._zod.bag;
325
- if (typeof minimum === "number")
326
- json.minItems = minimum;
327
- if (typeof maximum === "number")
328
- json.maxItems = maximum;
329
- break;
330
- }
331
- case "record": {
332
- const json = _json;
333
- json.type = "object";
334
- if (this.target === "draft-7" || this.target === "draft-2020-12") {
335
- json.propertyNames = this.process(def.keyType, {
336
- ...params,
337
- path: [...params.path, "propertyNames"],
338
- });
339
- }
340
- json.additionalProperties = this.process(def.valueType, {
341
- ...params,
342
- path: [...params.path, "additionalProperties"],
343
- });
344
- break;
345
- }
346
- case "map": {
347
- if (this.unrepresentable === "throw") {
348
- throw new Error("Map cannot be represented in JSON Schema");
349
- }
350
- break;
351
- }
352
- case "set": {
353
- if (this.unrepresentable === "throw") {
354
- throw new Error("Set cannot be represented in JSON Schema");
355
- }
356
- break;
357
- }
358
- case "enum": {
359
- const json = _json;
360
- const values = getEnumValues(def.entries);
361
- // Number enums can have both string and number values
362
- if (values.every((v) => typeof v === "number"))
363
- json.type = "number";
364
- if (values.every((v) => typeof v === "string"))
365
- json.type = "string";
366
- json.enum = values;
367
- break;
368
- }
369
- case "literal": {
370
- const json = _json;
371
- const vals = [];
372
- for (const val of def.values) {
373
- if (val === undefined) {
374
- if (this.unrepresentable === "throw") {
375
- throw new Error("Literal `undefined` cannot be represented in JSON Schema");
376
- }
377
- else {
378
- // do not add to vals
379
- }
380
- }
381
- else if (typeof val === "bigint") {
382
- if (this.unrepresentable === "throw") {
383
- throw new Error("BigInt literals cannot be represented in JSON Schema");
384
- }
385
- else {
386
- vals.push(Number(val));
387
- }
388
- }
389
- else {
390
- vals.push(val);
391
- }
392
- }
393
- if (vals.length === 0) {
394
- // do nothing (an undefined literal was stripped)
395
- }
396
- else if (vals.length === 1) {
397
- const val = vals[0];
398
- json.type = val === null ? "null" : typeof val;
399
- if (this.target === "draft-4" || this.target === "openapi-3.0") {
400
- json.enum = [val];
401
- }
402
- else {
403
- json.const = val;
404
- }
405
- }
406
- else {
407
- if (vals.every((v) => typeof v === "number"))
408
- json.type = "number";
409
- if (vals.every((v) => typeof v === "string"))
410
- json.type = "string";
411
- if (vals.every((v) => typeof v === "boolean"))
412
- json.type = "string";
413
- if (vals.every((v) => v === null))
414
- json.type = "null";
415
- json.enum = vals;
416
- }
417
- break;
418
- }
419
- case "file": {
420
- const json = _json;
421
- const file = {
422
- type: "string",
423
- format: "binary",
424
- contentEncoding: "binary",
425
- };
426
- const { minimum, maximum, mime } = schema._zod.bag;
427
- if (minimum !== undefined)
428
- file.minLength = minimum;
429
- if (maximum !== undefined)
430
- file.maxLength = maximum;
431
- if (mime) {
432
- if (mime.length === 1) {
433
- file.contentMediaType = mime[0];
434
- Object.assign(json, file);
435
- }
436
- else {
437
- json.anyOf = mime.map((m) => {
438
- const mFile = { ...file, contentMediaType: m };
439
- return mFile;
440
- });
441
- }
442
- }
443
- else {
444
- Object.assign(json, file);
445
- }
446
- // if (this.unrepresentable === "throw") {
447
- // throw new Error("File cannot be represented in JSON Schema");
448
- // }
449
- break;
450
- }
451
- case "transform": {
452
- if (this.unrepresentable === "throw") {
453
- throw new Error("Transforms cannot be represented in JSON Schema");
454
- }
455
- break;
456
- }
457
- case "nullable": {
458
- const inner = this.process(def.innerType, params);
459
- if (this.target === "openapi-3.0") {
460
- result.ref = def.innerType;
461
- _json.nullable = true;
462
- }
463
- else {
464
- _json.anyOf = [inner, { type: "null" }];
465
- }
466
- break;
467
- }
468
- case "nonoptional": {
469
- this.process(def.innerType, params);
470
- result.ref = def.innerType;
471
- break;
472
- }
473
- case "success": {
474
- const json = _json;
475
- json.type = "boolean";
476
- break;
477
- }
478
- case "default": {
479
- this.process(def.innerType, params);
480
- result.ref = def.innerType;
481
- _json.default = JSON.parse(JSON.stringify(def.defaultValue));
482
- break;
483
- }
484
- case "prefault": {
485
- this.process(def.innerType, params);
486
- result.ref = def.innerType;
487
- if (this.io === "input")
488
- _json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
489
- break;
490
- }
491
- case "catch": {
492
- // use conditionals
493
- this.process(def.innerType, params);
494
- result.ref = def.innerType;
495
- let catchValue;
496
- try {
497
- catchValue = def.catchValue(undefined);
498
- }
499
- catch {
500
- throw new Error("Dynamic catch values are not supported in JSON Schema");
501
- }
502
- _json.default = catchValue;
503
- break;
504
- }
505
- case "nan": {
506
- if (this.unrepresentable === "throw") {
507
- throw new Error("NaN cannot be represented in JSON Schema");
508
- }
509
- break;
510
- }
511
- case "template_literal": {
512
- const json = _json;
513
- const pattern = schema._zod.pattern;
514
- if (!pattern)
515
- throw new Error("Pattern not found in template literal");
516
- json.type = "string";
517
- json.pattern = pattern.source;
518
- break;
519
- }
520
- case "pipe": {
521
- const innerType = this.io === "input" ? (def.in._zod.def.type === "transform" ? def.out : def.in) : def.out;
522
- this.process(innerType, params);
523
- result.ref = innerType;
524
- break;
525
- }
526
- case "readonly": {
527
- this.process(def.innerType, params);
528
- result.ref = def.innerType;
529
- _json.readOnly = true;
530
- break;
531
- }
532
- // passthrough types
533
- case "promise": {
534
- this.process(def.innerType, params);
535
- result.ref = def.innerType;
536
- break;
537
- }
538
- case "optional": {
539
- this.process(def.innerType, params);
540
- result.ref = def.innerType;
541
- break;
542
- }
543
- case "lazy": {
544
- const innerType = schema._zod.innerType;
545
- this.process(innerType, params);
546
- result.ref = innerType;
547
- break;
548
- }
549
- case "custom": {
550
- if (this.unrepresentable === "throw") {
551
- throw new Error("Custom types cannot be represented in JSON Schema");
552
- }
553
- break;
554
- }
555
- case "function": {
556
- if (this.unrepresentable === "throw") {
557
- throw new Error("Function types cannot be represented in JSON Schema");
558
- }
559
- break;
560
- }
561
- default: {
562
- def;
563
- }
564
- }
70
+ const _json = result.schema;
71
+ const processor = ctx.processors[def.type];
72
+ if (!processor) {
73
+ throw new Error(`[toJSONSchema]: Non-representable type encountered: ${def.type}`);
565
74
  }
75
+ processor(schema, ctx, _json, params);
566
76
  }
567
- // metadata
568
- const meta = this.metadataRegistry.get(schema);
569
- if (meta)
570
- Object.assign(result.schema, meta);
571
- if (this.io === "input" && isTransforming(schema)) {
572
- // examples/defaults only apply to output type of pipe
573
- delete result.schema.examples;
574
- delete result.schema.default;
575
- }
576
- // set prefault as default
577
- if (this.io === "input" && result.schema._prefault)
578
- (_a = result.schema).default ?? (_a.default = result.schema._prefault);
579
- delete result.schema._prefault;
580
- // pulling fresh from this.seen in case it was overwritten
581
- const _result = this.seen.get(schema);
582
- return _result.schema;
583
77
  }
584
- emit(schema, _params) {
585
- const params = {
586
- cycles: _params?.cycles ?? "ref",
587
- reused: _params?.reused ?? "inline",
588
- // unrepresentable: _params?.unrepresentable ?? "throw",
589
- // uri: _params?.uri ?? ((id) => `${id}`),
590
- external: _params?.external ?? undefined,
591
- };
592
- // iterate over seen map;
593
- const root = this.seen.get(schema);
594
- if (!root)
595
- throw new Error("Unprocessed schema. This is a bug in Zod.");
596
- // initialize result with root schema fields
597
- // Object.assign(result, seen.cached);
598
- // returns a ref to the schema
599
- // defId will be empty if the ref points to an external schema (or #)
600
- const makeURI = (entry) => {
601
- // comparing the seen objects because sometimes
602
- // multiple schemas map to the same seen object.
603
- // e.g. lazy
604
- // external is configured
605
- const defsSegment = this.target === "draft-2020-12" ? "$defs" : "definitions";
606
- if (params.external) {
607
- const externalId = params.external.registry.get(entry[0])?.id; // ?? "__shared";// `__schema${this.counter++}`;
608
- // check if schema is in the external registry
609
- const uriGenerator = params.external.uri ?? ((id) => id);
610
- if (externalId) {
611
- return { ref: uriGenerator(externalId) };
612
- }
613
- // otherwise, add to __shared
614
- const id = entry[1].defId ?? entry[1].schema.id ?? `schema${this.counter++}`;
615
- entry[1].defId = id; // set defId so it will be reused if needed
616
- return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${id}` };
617
- }
618
- if (entry[1] === root) {
619
- return { ref: "#" };
620
- }
621
- // self-contained schema
622
- const uriPrefix = `#`;
623
- const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
624
- const defId = entry[1].schema.id ?? `__schema${this.counter++}`;
625
- return { defId, ref: defUriPrefix + defId };
626
- };
627
- // stored cached version in `def` property
628
- // remove all properties, set $ref
629
- const extractToDef = (entry) => {
630
- // if the schema is already a reference, do not extract it
631
- if (entry[1].schema.$ref) {
632
- return;
633
- }
634
- const seen = entry[1];
635
- const { ref, defId } = makeURI(entry);
636
- seen.def = { ...seen.schema };
637
- // defId won't be set if the schema is a reference to an external schema
638
- if (defId)
639
- seen.defId = defId;
640
- // wipe away all properties except $ref
641
- const schema = seen.schema;
642
- for (const key in schema) {
643
- delete schema[key];
644
- }
645
- schema.$ref = ref;
646
- };
647
- // throw on cycles
648
- // break cycles
649
- if (params.cycles === "throw") {
650
- for (const entry of this.seen.entries()) {
651
- const seen = entry[1];
652
- if (seen.cycle) {
653
- throw new Error("Cycle detected: " +
654
- `#/${seen.cycle?.join("/")}/<root>` +
655
- '\n\nSet the `cycles` parameter to `"ref"` to resolve cyclical schemas with defs.');
656
- }
657
- }
658
- }
659
- // extract schemas into $defs
660
- for (const entry of this.seen.entries()) {
661
- const seen = entry[1];
662
- // convert root schema to # $ref
663
- if (schema === entry[0]) {
664
- extractToDef(entry); // this has special handling for the root schema
665
- continue;
666
- }
667
- // extract schemas that are in the external registry
668
- if (params.external) {
669
- const ext = params.external.registry.get(entry[0])?.id;
670
- if (schema !== entry[0] && ext) {
671
- extractToDef(entry);
672
- continue;
673
- }
674
- }
675
- // extract schemas with `id` meta
676
- const id = this.metadataRegistry.get(entry[0])?.id;
677
- if (id) {
678
- extractToDef(entry);
679
- continue;
680
- }
681
- // break cycles
682
- if (seen.cycle) {
683
- // any
684
- extractToDef(entry);
685
- continue;
686
- }
687
- // extract reused schemas
688
- if (seen.count > 1) {
689
- if (params.reused === "ref") {
690
- extractToDef(entry);
691
- // biome-ignore lint:
692
- continue;
693
- }
694
- }
695
- }
696
- // flatten _refs
697
- const flattenRef = (zodSchema, params) => {
698
- const seen = this.seen.get(zodSchema);
699
- const schema = seen.def ?? seen.schema;
700
- const _cached = { ...schema };
701
- // already seen
702
- if (seen.ref === null) {
703
- return;
704
- }
705
- // flatten ref if defined
706
- const ref = seen.ref;
707
- seen.ref = null; // prevent recursion
708
- if (ref) {
709
- flattenRef(ref, params);
710
- // merge referenced schema into current
711
- const refSchema = this.seen.get(ref).schema;
712
- if (refSchema.$ref &&
713
- (params.target === "draft-7" || params.target === "draft-4" || params.target === "openapi-3.0")) {
714
- schema.allOf = schema.allOf ?? [];
715
- schema.allOf.push(refSchema);
716
- }
717
- else {
718
- Object.assign(schema, refSchema);
719
- Object.assign(schema, _cached); // prevent overwriting any fields in the original schema
720
- }
78
+ // metadata
79
+ const meta = ctx.metadataRegistry.get(schema);
80
+ if (meta)
81
+ Object.assign(result.schema, meta);
82
+ if (ctx.io === "input" && isTransforming(schema)) {
83
+ // examples/defaults only apply to output type of pipe
84
+ delete result.schema.examples;
85
+ delete result.schema.default;
86
+ }
87
+ // set prefault as default
88
+ if (ctx.io === "input" && result.schema._prefault)
89
+ (_a = result.schema).default ?? (_a.default = result.schema._prefault);
90
+ delete result.schema._prefault;
91
+ // pulling fresh from ctx.seen in case it was overwritten
92
+ const _result = ctx.seen.get(schema);
93
+ return _result.schema;
94
+ }
95
+ export function extractDefs(ctx, schema
96
+ // params: EmitParams
97
+ ) {
98
+ // iterate over seen map;
99
+ const root = ctx.seen.get(schema);
100
+ if (!root)
101
+ throw new Error("Unprocessed schema. This is a bug in Zod.");
102
+ // returns a ref to the schema
103
+ // defId will be empty if the ref points to an external schema (or #)
104
+ const makeURI = (entry) => {
105
+ // comparing the seen objects because sometimes
106
+ // multiple schemas map to the same seen object.
107
+ // e.g. lazy
108
+ // external is configured
109
+ const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
110
+ if (ctx.external) {
111
+ const externalId = ctx.external.registry.get(entry[0])?.id; // ?? "__shared";// `__schema${ctx.counter++}`;
112
+ // check if schema is in the external registry
113
+ const uriGenerator = ctx.external.uri ?? ((id) => id);
114
+ if (externalId) {
115
+ return { ref: uriGenerator(externalId) };
721
116
  }
722
- // execute overrides
723
- if (!seen.isParent)
724
- this.override({
725
- zodSchema: zodSchema,
726
- jsonSchema: schema,
727
- path: seen.path ?? [],
728
- });
729
- };
730
- for (const entry of [...this.seen.entries()].reverse()) {
731
- flattenRef(entry[0], { target: this.target });
732
- }
733
- const result = {};
734
- if (this.target === "draft-2020-12") {
735
- result.$schema = "https://json-schema.org/draft/2020-12/schema";
117
+ // otherwise, add to __shared
118
+ const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;
119
+ entry[1].defId = id; // set defId so it will be reused if needed
120
+ return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${id}` };
736
121
  }
737
- else if (this.target === "draft-7") {
738
- result.$schema = "http://json-schema.org/draft-07/schema#";
122
+ if (entry[1] === root) {
123
+ return { ref: "#" };
739
124
  }
740
- else if (this.target === "draft-4") {
741
- result.$schema = "http://json-schema.org/draft-04/schema#";
125
+ // self-contained schema
126
+ const uriPrefix = `#`;
127
+ const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
128
+ const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;
129
+ return { defId, ref: defUriPrefix + defId };
130
+ };
131
+ // stored cached version in `def` property
132
+ // remove all properties, set $ref
133
+ const extractToDef = (entry) => {
134
+ // if the schema is already a reference, do not extract it
135
+ if (entry[1].schema.$ref) {
136
+ return;
742
137
  }
743
- else if (this.target === "openapi-3.0") {
744
- // OpenAPI 3.0 schema objects should not include a $schema property
745
- }
746
- else {
747
- // @ts-ignore
748
- console.warn(`Invalid target: ${this.target}`);
138
+ const seen = entry[1];
139
+ const { ref, defId } = makeURI(entry);
140
+ seen.def = { ...seen.schema };
141
+ // defId won't be set if the schema is a reference to an external schema
142
+ // or if the schema is the root schema
143
+ if (defId)
144
+ seen.defId = defId;
145
+ // wipe away all properties except $ref
146
+ const schema = seen.schema;
147
+ for (const key in schema) {
148
+ delete schema[key];
749
149
  }
750
- if (params.external?.uri) {
751
- const id = params.external.registry.get(schema)?.id;
752
- if (!id)
753
- throw new Error("Schema is missing an `id` property");
754
- result.$id = params.external.uri(id);
755
- }
756
- Object.assign(result, root.def);
757
- // build defs object
758
- const defs = params.external?.defs ?? {};
759
- for (const entry of this.seen.entries()) {
150
+ schema.$ref = ref;
151
+ };
152
+ // throw on cycles
153
+ // break cycles
154
+ if (ctx.cycles === "throw") {
155
+ for (const entry of ctx.seen.entries()) {
760
156
  const seen = entry[1];
761
- if (seen.def && seen.defId) {
762
- defs[seen.defId] = seen.def;
157
+ if (seen.cycle) {
158
+ throw new Error("Cycle detected: " +
159
+ `#/${seen.cycle?.join("/")}/<root>` +
160
+ '\n\nSet the `cycles` parameter to `"ref"` to resolve cyclical schemas with defs.');
763
161
  }
764
162
  }
765
- // set definitions in result
766
- if (params.external) {
163
+ }
164
+ // extract schemas into $defs
165
+ for (const entry of ctx.seen.entries()) {
166
+ const seen = entry[1];
167
+ // convert root schema to # $ref
168
+ if (schema === entry[0]) {
169
+ extractToDef(entry); // this has special handling for the root schema
170
+ continue;
767
171
  }
768
- else {
769
- if (Object.keys(defs).length > 0) {
770
- if (this.target === "draft-2020-12") {
771
- result.$defs = defs;
772
- }
773
- else {
774
- result.definitions = defs;
775
- }
172
+ // extract schemas that are in the external registry
173
+ if (ctx.external) {
174
+ const ext = ctx.external.registry.get(entry[0])?.id;
175
+ if (schema !== entry[0] && ext) {
176
+ extractToDef(entry);
177
+ continue;
776
178
  }
777
179
  }
778
- try {
779
- // this "finalizes" this schema and ensures all cycles are removed
780
- // each call to .emit() is functionally independent
781
- // though the seen map is shared
782
- return JSON.parse(JSON.stringify(result));
180
+ // extract schemas with `id` meta
181
+ const id = ctx.metadataRegistry.get(entry[0])?.id;
182
+ if (id) {
183
+ extractToDef(entry);
184
+ continue;
783
185
  }
784
- catch (_err) {
785
- throw new Error("Error converting schema to JSON.");
186
+ // break cycles
187
+ if (seen.cycle) {
188
+ // any
189
+ extractToDef(entry);
190
+ continue;
191
+ }
192
+ // extract reused schemas
193
+ if (seen.count > 1) {
194
+ if (ctx.reused === "ref") {
195
+ extractToDef(entry);
196
+ // biome-ignore lint:
197
+ continue;
198
+ }
786
199
  }
787
200
  }
788
201
  }
789
- export function toJSONSchema(input, _params) {
790
- if (input instanceof $ZodRegistry) {
791
- const gen = new JSONSchemaGenerator(_params);
792
- const defs = {};
793
- for (const entry of input._idmap.entries()) {
794
- const [_, schema] = entry;
795
- gen.process(schema);
202
+ export function finalize(ctx, schema) {
203
+ //
204
+ // iterate over seen map;
205
+ const root = ctx.seen.get(schema);
206
+ if (!root)
207
+ throw new Error("Unprocessed schema. This is a bug in Zod.");
208
+ // flatten _refs
209
+ const flattenRef = (zodSchema) => {
210
+ const seen = ctx.seen.get(zodSchema);
211
+ const schema = seen.def ?? seen.schema;
212
+ const _cached = { ...schema };
213
+ // already seen
214
+ if (seen.ref === null) {
215
+ return;
796
216
  }
797
- const schemas = {};
798
- const external = {
799
- registry: input,
800
- uri: _params?.uri,
801
- defs,
802
- };
803
- for (const entry of input._idmap.entries()) {
804
- const [key, schema] = entry;
805
- schemas[key] = gen.emit(schema, {
806
- ..._params,
807
- external,
217
+ // flatten ref if defined
218
+ const ref = seen.ref;
219
+ seen.ref = null; // prevent recursion
220
+ if (ref) {
221
+ flattenRef(ref);
222
+ // merge referenced schema into current
223
+ const refSchema = ctx.seen.get(ref).schema;
224
+ if (refSchema.$ref && (ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0")) {
225
+ schema.allOf = schema.allOf ?? [];
226
+ schema.allOf.push(refSchema);
227
+ }
228
+ else {
229
+ Object.assign(schema, refSchema);
230
+ Object.assign(schema, _cached); // prevent overwriting any fields in the original schema
231
+ }
232
+ }
233
+ // execute overrides
234
+ if (!seen.isParent)
235
+ ctx.override({
236
+ zodSchema: zodSchema,
237
+ jsonSchema: schema,
238
+ path: seen.path ?? [],
808
239
  });
240
+ };
241
+ for (const entry of [...ctx.seen.entries()].reverse()) {
242
+ flattenRef(entry[0]);
243
+ }
244
+ const result = {};
245
+ if (ctx.target === "draft-2020-12") {
246
+ result.$schema = "https://json-schema.org/draft/2020-12/schema";
247
+ }
248
+ else if (ctx.target === "draft-07") {
249
+ result.$schema = "http://json-schema.org/draft-07/schema#";
250
+ }
251
+ else if (ctx.target === "draft-04") {
252
+ result.$schema = "http://json-schema.org/draft-04/schema#";
253
+ }
254
+ else if (ctx.target === "openapi-3.0") {
255
+ // OpenAPI 3.0 schema objects should not include a $schema property
256
+ }
257
+ else {
258
+ // Arbitrary string values are allowed but won't have a $schema property set
259
+ }
260
+ if (ctx.external?.uri) {
261
+ const id = ctx.external.registry.get(schema)?.id;
262
+ if (!id)
263
+ throw new Error("Schema is missing an `id` property");
264
+ result.$id = ctx.external.uri(id);
265
+ }
266
+ Object.assign(result, root.def ?? root.schema);
267
+ // build defs object
268
+ const defs = ctx.external?.defs ?? {};
269
+ for (const entry of ctx.seen.entries()) {
270
+ const seen = entry[1];
271
+ if (seen.def && seen.defId) {
272
+ defs[seen.defId] = seen.def;
809
273
  }
274
+ }
275
+ // set definitions in result
276
+ if (ctx.external) {
277
+ }
278
+ else {
810
279
  if (Object.keys(defs).length > 0) {
811
- const defsSegment = gen.target === "draft-2020-12" ? "$defs" : "definitions";
812
- schemas.__shared = {
813
- [defsSegment]: defs,
814
- };
280
+ if (ctx.target === "draft-2020-12") {
281
+ result.$defs = defs;
282
+ }
283
+ else {
284
+ result.definitions = defs;
285
+ }
815
286
  }
816
- return { schemas };
817
287
  }
818
- const gen = new JSONSchemaGenerator(_params);
819
- gen.process(input);
820
- return gen.emit(input, _params);
288
+ try {
289
+ // this "finalizes" this schema and ensures all cycles are removed
290
+ // each call to finalize() is functionally independent
291
+ // though the seen map is shared
292
+ const finalized = JSON.parse(JSON.stringify(result));
293
+ Object.defineProperty(finalized, "~standard", {
294
+ value: {
295
+ ...schema["~standard"],
296
+ jsonSchema: {
297
+ input: createStandardJSONSchemaMethod(schema, "input"),
298
+ output: createStandardJSONSchemaMethod(schema, "output"),
299
+ },
300
+ },
301
+ enumerable: false,
302
+ writable: false,
303
+ });
304
+ return finalized;
305
+ }
306
+ catch (_err) {
307
+ throw new Error("Error converting schema to JSON.");
308
+ }
821
309
  }
822
310
  function isTransforming(_schema, _ctx) {
823
311
  const ctx = _ctx ?? { seen: new Set() };
@@ -876,3 +364,20 @@ function isTransforming(_schema, _ctx) {
876
364
  }
877
365
  return false;
878
366
  }
367
+ /**
368
+ * Creates a toJSONSchema method for a schema instance.
369
+ * This encapsulates the logic of initializing context, processing, extracting defs, and finalizing.
370
+ */
371
+ export const createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
372
+ const ctx = initializeContext({ ...params, processors });
373
+ process(schema, ctx);
374
+ extractDefs(ctx, schema);
375
+ return finalize(ctx, schema);
376
+ };
377
+ export const createStandardJSONSchemaMethod = (schema, io) => (params) => {
378
+ const { libraryOptions, target } = params ?? {};
379
+ const ctx = initializeContext({ ...(libraryOptions ?? {}), target, io, processors: {} });
380
+ process(schema, ctx);
381
+ extractDefs(ctx, schema);
382
+ return finalize(ctx, schema);
383
+ };