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