From e9e27905cc0f37cb079ea473af8359d5e17a57a1 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Thu, 16 Oct 2025 15:30:06 -0700 Subject: [PATCH 1/3] Clean up comment --- packages/zod/src/v4/classic/schemas.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts index e6dc04ee5..f9e3510ea 100644 --- a/packages/zod/src/v4/classic/schemas.ts +++ b/packages/zod/src/v4/classic/schemas.ts @@ -265,7 +265,6 @@ export const _ZodString: core.$constructor<_ZodString> = /*@__PURE__*/ core.$con inst.maxLength = bag.maximum ?? null; // validations - // if (inst.regex) throw new Error("regex already defined"); inst.regex = (...args) => inst.check(checks.regex(...args)); inst.includes = (...args) => inst.check(checks.includes(...args)); inst.startsWith = (...args) => inst.check(checks.startsWith(...args)); From 602cf7820ff95917e9fd530f18de884ab1f10693 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Thu, 16 Oct 2025 15:34:47 -0700 Subject: [PATCH 2/3] Refine JSON Schema helpers per schema --- packages/zod/src/v4/core/checks.ts | 17 +- .../zod/src/v4/core/json-schema-lite.test.ts | 451 ++++++++++++++++++ packages/zod/src/v4/core/json-schema-lite.ts | 28 ++ packages/zod/src/v4/core/registries.ts | 9 +- packages/zod/src/v4/core/schemas.ts | 161 ++++++- 5 files changed, 648 insertions(+), 18 deletions(-) create mode 100644 packages/zod/src/v4/core/json-schema-lite.test.ts create mode 100644 packages/zod/src/v4/core/json-schema-lite.ts diff --git a/packages/zod/src/v4/core/checks.ts b/packages/zod/src/v4/core/checks.ts index 8c5383b58..e82c063b8 100644 --- a/packages/zod/src/v4/core/checks.ts +++ b/packages/zod/src/v4/core/checks.ts @@ -611,8 +611,8 @@ export const $ZodCheckMaxLength: core.$constructor<$ZodCheckMaxLength> = /*@__PU }; inst._zod.onattach.push((inst) => { - const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY) as number; - if (def.maximum < curr) inst._zod.bag.maximum = def.maximum; + const curr = (inst._zod.bag.maxLength ?? Number.POSITIVE_INFINITY) as number; + if (def.maximum < curr) inst._zod.bag.maxLength = def.maximum; }); inst._zod.check = (payload) => { @@ -662,8 +662,8 @@ export const $ZodCheckMinLength: core.$constructor<$ZodCheckMinLength> = /*@__PU }; inst._zod.onattach.push((inst) => { - const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY) as number; - if (def.minimum > curr) inst._zod.bag.minimum = def.minimum; + const curr = (inst._zod.bag.minLength ?? Number.NEGATIVE_INFINITY) as number; + if (def.minimum > curr) inst._zod.bag.minLength = def.minimum; }); inst._zod.check = (payload) => { @@ -715,9 +715,8 @@ export const $ZodCheckLengthEquals: core.$constructor<$ZodCheckLengthEquals> = / inst._zod.onattach.push((inst) => { const bag = inst._zod.bag; - bag.minimum = def.length; - bag.maximum = def.length; - bag.length = def.length; + bag.minLength = def.length; + bag.maxLength = def.length; }); inst._zod.check = (payload) => { @@ -799,6 +798,7 @@ export const $ZodCheckStringFormat: core.$constructor<$ZodCheckStringFormat> = / if (def.pattern) { bag.patterns ??= new Set(); bag.patterns.add(def.pattern); + bag.pattern = def.pattern.source; } }); @@ -967,6 +967,7 @@ export const $ZodCheckIncludes: core.$constructor<$ZodCheckIncludes> = /*@__PURE const bag = inst._zod.bag as schemas.$ZodStringInternals["bag"]; bag.patterns ??= new Set(); bag.patterns.add(pattern); + bag.pattern = pattern.source; }); inst._zod.check = (payload) => { @@ -1011,6 +1012,7 @@ export const $ZodCheckStartsWith: core.$constructor<$ZodCheckStartsWith> = /*@__ const bag = inst._zod.bag as schemas.$ZodStringInternals["bag"]; bag.patterns ??= new Set(); bag.patterns.add(pattern); + bag.pattern = pattern.source; }); inst._zod.check = (payload) => { @@ -1055,6 +1057,7 @@ export const $ZodCheckEndsWith: core.$constructor<$ZodCheckEndsWith> = /*@__PURE const bag = inst._zod.bag as schemas.$ZodStringInternals["bag"]; bag.patterns ??= new Set(); bag.patterns.add(pattern); + bag.pattern = pattern.source; }); inst._zod.check = (payload) => { diff --git a/packages/zod/src/v4/core/json-schema-lite.test.ts b/packages/zod/src/v4/core/json-schema-lite.test.ts new file mode 100644 index 000000000..299a0dd35 --- /dev/null +++ b/packages/zod/src/v4/core/json-schema-lite.test.ts @@ -0,0 +1,451 @@ +import { describe, expect, it } from "vitest"; +import * as z from "../classic/index.js"; + +describe("JSON Schema Generation", () => { + describe("Primitives", () => { + it("string", () => { + const schema = z.string(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "type": "string", + } + `); + }); + + it("string with minLength", () => { + const schema = z.string().min(5); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "minLength": 5, + "type": "string", + } + `); + }); + + it("string with maxLength", () => { + const schema = z.string().max(10); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "maxLength": 10, + "type": "string", + } + `); + }); + + it("string with length constraints", () => { + const schema = z.string().min(5).max(10); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "maxLength": 10, + "minLength": 5, + "type": "string", + } + `); + }); + + it("email", () => { + const schema = z.email(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "type": "string", + } + `); + }); + + it("url", () => { + const schema = z.url(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "format": "uri", + "type": "string", + } + `); + }); + + it("uuid", () => { + const schema = z.uuid(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "type": "string", + } + `); + }); + + it("number", () => { + const schema = z.number(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "type": "number", + } + `); + }); + + it("number with min", () => { + const schema = z.number().min(0); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "minimum": 0, + "type": "number", + } + `); + }); + + it("number with max", () => { + const schema = z.number().max(100); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "maximum": 100, + "type": "number", + } + `); + }); + + it("number with gt (exclusive)", () => { + const schema = z.number().gt(0); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "exclusiveMinimum": 0, + "type": "number", + } + `); + }); + + it("number with lt (exclusive)", () => { + const schema = z.number().lt(100); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "exclusiveMaximum": 100, + "type": "number", + } + `); + }); + + it("int", () => { + const schema = z.int(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "format": "safeint", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "pattern": /\\^-\\?\\\\d\\+\\$/, + "type": "integer", + } + `); + }); + + it("boolean", () => { + const schema = z.boolean(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "type": "boolean", + } + `); + }); + + it("null", () => { + const schema = z.null(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "type": "null", + } + `); + }); + + it("bigint throws", () => { + const schema = z.bigint(); + expect(() => schema._zod.getJSONSchema()).toThrow('Unsupported JSON Schema conversion for type "bigint"'); + }); + }); + + describe("Arrays", () => { + it("array of strings", () => { + const schema = z.array(z.string()); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "items": { + "type": "string", + }, + "type": "array", + } + `); + }); + + it("array with minLength", () => { + const schema = z.array(z.string()).min(1); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "items": { + "type": "string", + }, + "minLength": 1, + "type": "array", + } + `); + }); + }); + + describe("Objects", () => { + it("simple object", () => { + const schema = z.object({ + name: z.string(), + age: z.number(), + }); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "properties": { + "age": { + "type": "number", + }, + "name": { + "type": "string", + }, + }, + "required": [ + "name", + "age", + ], + "type": "object", + } + `); + }); + + it("object with optional field", () => { + const schema = z.object({ + name: z.string(), + age: z.number().optional(), + }); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "properties": { + "age": { + "type": "number", + }, + "name": { + "type": "string", + }, + }, + "required": [ + "name", + ], + "type": "object", + } + `); + }); + + it("nested object", () => { + const schema = z.object({ + user: z.object({ + name: z.string(), + email: z.email(), + }), + }); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "properties": { + "user": { + "properties": { + "email": { + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "type": "string", + }, + "name": { + "type": "string", + }, + }, + "required": [ + "name", + "email", + ], + "type": "object", + }, + }, + "required": [ + "user", + ], + "type": "object", + } + `); + }); + }); + + describe("Unions", () => { + it("string or number", () => { + const schema = z.union([z.string(), z.number()]); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "anyOf": [ + { + "type": "string", + }, + { + "type": "number", + }, + ], + } + `); + }); + + it("nullable", () => { + const schema = z.string().nullable(); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + } + `); + }); + }); + + describe("Enums", () => { + it("string enum", () => { + const schema = z.enum(["red", "green", "blue"]); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "enum": [ + "red", + "green", + "blue", + ], + } + `); + }); + }); + + describe("Literals", () => { + it("string literal", () => { + const schema = z.literal("hello"); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "enum": [ + "hello", + ], + } + `); + }); + + it("number literal", () => { + const schema = z.literal(42); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "enum": [ + 42, + ], + } + `); + }); + }); + + describe("Tuples", () => { + it("simple tuple", () => { + const schema = z.tuple([z.string(), z.number()]); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "items": false, + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "type": "string", + }, + { + "type": "number", + }, + ], + "type": "array", + } + `); + }); + }); + + describe("Records", () => { + it("record with string values", () => { + const schema = z.record(z.string(), z.number()); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "additionalProperties": { + "type": "number", + }, + "propertyNames": { + "type": "string", + }, + "type": "object", + } + `); + }); + }); + + describe("Caching", () => { + it("caches result for same schema instance", () => { + const schema = z.string(); + const result1 = schema._zod.getJSONSchema(); + const result2 = schema._zod.getJSONSchema(); + expect(result1).toBe(result2); // Same object reference + }); + + it("handles recursive schemas", () => { + interface Category { + name: string; + subcategories?: Category[]; + } + const Category: any = z.lazy(() => + z.object({ + name: z.string(), + subcategories: z.array(Category).optional(), + }) + ); + + expect(Category._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "properties": { + "name": { + "type": "string", + }, + "subcategories": { + "items": [Circular], + "type": "array", + }, + }, + "required": [ + "name", + ], + "type": "object", + } + `); + }); + }); + + describe("Metadata", () => { + it("includes description from registry", () => { + const schema = z.string().describe("User's name"); + expect(schema._zod.getJSONSchema()).toMatchInlineSnapshot(` + { + "description": "User's name", + "type": "string", + } + `); + }); + }); + + describe("Unsupported types", () => { + it("symbol throws", () => { + const schema = z.symbol(); + expect(() => schema._zod.getJSONSchema()).toThrow('Unsupported JSON Schema conversion for type "symbol"'); + }); + + it("function throws", () => { + const schema = z.function(); + expect(() => schema._zod.getJSONSchema()).toThrow('Unsupported JSON Schema conversion for type "function"'); + }); + }); +}); diff --git a/packages/zod/src/v4/core/json-schema-lite.ts b/packages/zod/src/v4/core/json-schema-lite.ts new file mode 100644 index 000000000..44cf26f5b --- /dev/null +++ b/packages/zod/src/v4/core/json-schema-lite.ts @@ -0,0 +1,28 @@ +import type * as JSONSchema from "./json-schema.js"; +import { globalRegistry } from "./registries.js"; +import type * as schemas from "./schemas.js"; + +const cache = new WeakMap(); + +export interface JSONSchemaContext { + // Reserved for future development +} + +export function toJSON( + schema: schemas.$ZodType, + ctx: JSONSchemaContext | undefined, + build: (json: T, ctx: JSONSchemaContext) => void +): T { + const cached = cache.get(schema); + if (cached) return cached as T; + + const json = {} as T; + cache.set(schema, json); + Object.assign(json, schema._zod.bag); + + const meta = globalRegistry._map.get(schema); + if (meta) Object.assign(json, meta); + + build(json, ctx ?? {}); + return json; +} diff --git a/packages/zod/src/v4/core/registries.ts b/packages/zod/src/v4/core/registries.ts index a4e8ae063..1745ec8fb 100644 --- a/packages/zod/src/v4/core/registries.ts +++ b/packages/zod/src/v4/core/registries.ts @@ -53,8 +53,8 @@ export class $ZodRegistry unknown; + /** Minimal JSON Schema representation for this schema. */ + getJSONSchema: (ctx?: JSONSchemaContext) => JSONSchema.BaseSchema; + /** @internal The parent of this schema. Only set during certain clone operations. */ parent?: $ZodType | undefined; } @@ -178,9 +184,16 @@ export interface _$ZodType export const $ZodType: core.$constructor<$ZodType> = /*@__PURE__*/ core.$constructor("$ZodType", (inst, def) => { inst ??= {} as any; + // @ts-ignore + inst.asdf = toJSONSchema; + inst._zod.def = def; // set _def property inst._zod.bag = inst._zod.bag || {}; // initialize _bag object inst._zod.version = version; + // Only set default getJSONSchema if not already set + inst._zod.getJSONSchema ??= () => { + throw new Error(`Unsupported JSON Schema conversion for type "${def.type}"`); + }; const checks = [...(inst._zod.def.checks ?? [])]; @@ -350,9 +363,28 @@ export interface $ZodString extends _$ZodType<$ZodStringInterna // _zod: $ZodStringInternals; } +const stringFormatMap: Partial> = { + guid: "uuid", + url: "uri", + datetime: "date-time", + json_string: "json-string", + regex: undefined, +}; + export const $ZodString: core.$constructor<$ZodString> = /*@__PURE__*/ core.$constructor("$ZodString", (inst, def) => { $ZodType.init(inst, def); inst._zod.pattern = [...(inst?._zod.bag?.patterns ?? [])].pop() ?? regexes.string(inst._zod.bag); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json) => { + json.type = "string"; + + // Apply format mapping + const format = json.format; + if (format) json.format = stringFormatMap[format as checks.$ZodStringFormats] ?? format; + + // Clean up non-JSON-Schema bag fields + delete (json as any).patterns; + }); inst._zod.parse = (payload, _) => { if (def.coerce) try { @@ -741,8 +773,10 @@ export interface $ZodIPv4 extends $ZodType { export const $ZodIPv4: core.$constructor<$ZodIPv4> = /*@__PURE__*/ core.$constructor("$ZodIPv4", (inst, def): void => { def.pattern ??= regexes.ipv4; $ZodStringFormat.init(inst, def); - - inst._zod.bag.format = `ipv4`; + inst._zod.onattach.push((inst) => { + const bag = inst._zod.bag as $ZodStringInternals["bag"]; + bag.format = `ipv4`; + }); }); ////////////////////////////// ZodIPv6 ////////////////////////////// @@ -763,7 +797,10 @@ export const $ZodIPv6: core.$constructor<$ZodIPv6> = /*@__PURE__*/ core.$constru def.pattern ??= regexes.ipv6; $ZodStringFormat.init(inst, def); - inst._zod.bag.format = `ipv6`; + inst._zod.onattach.push((inst) => { + const bag = inst._zod.bag as $ZodStringInternals["bag"]; + bag.format = `ipv6`; + }); inst._zod.check = (payload) => { try { @@ -874,7 +911,9 @@ export const $ZodBase64: core.$constructor<$ZodBase64> = /*@__PURE__*/ core.$con def.pattern ??= regexes.base64; $ZodStringFormat.init(inst, def); - inst._zod.bag.contentEncoding = "base64"; + inst._zod.onattach.push((inst) => { + inst._zod.bag.contentEncoding = "base64"; + }); inst._zod.check = (payload) => { if (isValidBase64(payload.value)) return; @@ -911,7 +950,9 @@ export const $ZodBase64URL: core.$constructor<$ZodBase64URL> = /*@__PURE__*/ cor def.pattern ??= regexes.base64url; $ZodStringFormat.init(inst, def); - inst._zod.bag.contentEncoding = "base64url"; + inst._zod.onattach.push((inst) => { + inst._zod.bag.contentEncoding = "base64url"; + }); inst._zod.check = (payload) => { if (isValidBase64URL(payload.value)) return; @@ -1056,8 +1097,16 @@ export interface $ZodNumber extends $ZodType { export const $ZodNumber: core.$constructor<$ZodNumber> = /*@__PURE__*/ core.$constructor("$ZodNumber", (inst, def) => { $ZodType.init(inst, def); - inst._zod.pattern = inst._zod.bag.pattern ?? regexes.number; + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json) => { + const bag = inst._zod.bag as $ZodNumberInternals["bag"] & { + multipleOf?: number; + }; + const format = bag?.format; + json.type = typeof format === "string" && format.includes("int") ? "integer" : "number"; + }); + inst._zod.parse = (payload, _ctx) => { if (def.coerce) try { @@ -1140,6 +1189,10 @@ export const $ZodBoolean: core.$constructor<$ZodBoolean> = /*@__PURE__*/ core.$c (inst, def) => { $ZodType.init(inst, def); inst._zod.pattern = regexes.boolean; + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json) => { + json.type = "boolean"; + }); inst._zod.parse = (payload, _ctx) => { if (def.coerce) @@ -1345,6 +1398,10 @@ export const $ZodNull: core.$constructor<$ZodNull> = /*@__PURE__*/ core.$constru $ZodType.init(inst, def); inst._zod.pattern = regexes.null; inst._zod.values = new Set([null]); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json) => { + json.type = "null"; + }); inst._zod.parse = (payload, _ctx) => { const input = payload.value; @@ -1383,6 +1440,7 @@ export interface $ZodAny extends $ZodType { export const $ZodAny: core.$constructor<$ZodAny> = /*@__PURE__*/ core.$constructor("$ZodAny", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => toJSON(inst, ctx, () => {}); inst._zod.parse = (payload) => payload; }); @@ -1412,6 +1470,7 @@ export const $ZodUnknown: core.$constructor<$ZodUnknown> = /*@__PURE__*/ core.$c "$ZodUnknown", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => toJSON(inst, ctx, () => {}); inst._zod.parse = (payload) => payload; } @@ -1518,6 +1577,13 @@ export interface $ZodDate extends $ZodType { export const $ZodDate: core.$constructor<$ZodDate> = /*@__PURE__*/ core.$constructor("$ZodDate", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json) => { + json.type = "string"; + json.format = "date-time"; + const bag = inst._zod.bag as $ZodDateInternals["bag"]; + if (bag?.format) json.format = bag.format; + }); inst._zod.parse = (payload, _ctx) => { if (def.coerce) { @@ -1575,6 +1641,14 @@ function handleArrayResult(result: ParsePayload, final: ParsePayload export const $ZodArray: core.$constructor<$ZodArray> = /*@__PURE__*/ core.$constructor("$ZodArray", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + json.type = "array"; + json.items = def.element._zod.getJSONSchema(context); + const bag = inst._zod.bag as { minimum?: number; maximum?: number }; + if (typeof bag?.minimum === "number") json.minItems = bag.minimum; + if (typeof bag?.maximum === "number") json.maxItems = bag.maximum; + }); inst._zod.parse = (payload, ctx) => { const input = payload.value; @@ -1816,6 +1890,26 @@ function handleCatchall( export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$constructor("$ZodObject", (inst, def) => { // requires cast because technically $ZodObject doesn't extend $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + json.type = "object"; + const shape = def.shape as $ZodShape; + const properties: Record = {}; + const required: string[] = []; + for (const key of Object.keys(shape)) { + const child = shape[key]!; + properties[key] = child._zod.getJSONSchema(context); + if (child._zod.optin !== "optional") { + required.push(key); + } + } + if (Object.keys(properties).length) json.properties = properties; + if (required.length) json.required = required; + const catchall = def.catchall as $ZodType | undefined; + if (catchall) { + json.additionalProperties = catchall._zod.def.type === "never" ? false : catchall._zod.getJSONSchema(context); + } + }); // const sh = def.shape; const desc = Object.getOwnPropertyDescriptor(def, "shape"); if (!desc?.get) { @@ -2046,6 +2140,10 @@ function handleUnionResults(results: ParsePayload[], final: ParsePayload, inst: export const $ZodUnion: core.$constructor<$ZodUnion> = /*@__PURE__*/ core.$constructor("$ZodUnion", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + json.anyOf = def.options.map((option) => option._zod.getJSONSchema(context)); + }); util.defineLazy(inst._zod, "optin", () => def.options.some((o) => o._zod.optin === "optional") ? "optional" : undefined @@ -2410,6 +2508,17 @@ export interface $ZodTuple< export const $ZodTuple: core.$constructor<$ZodTuple> = /*@__PURE__*/ core.$constructor("$ZodTuple", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + json.type = "array"; + const items = def.items.map((item) => item._zod.getJSONSchema(context)); + if (items.length) json.prefixItems = items; + if (def.rest) json.items = def.rest._zod.getJSONSchema(context); + else json.items = false; + const required = def.items.filter((item) => item._zod.optin !== "optional").length; + if (required) json.minItems = required; + if (!def.rest) json.maxItems = def.items.length; + }); const items = def.items; const optStart = items.length - [...items].reverse().findIndex((item) => item._zod.optin !== "optional"); @@ -2564,6 +2673,12 @@ export interface $ZodRecord = /*@__PURE__*/ core.$constructor("$ZodRecord", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + json.type = "object"; + json.additionalProperties = def.valueType._zod.getJSONSchema(context); + json.propertyNames = def.keyType._zod.getJSONSchema(context); + }); inst._zod.parse = (payload, ctx) => { const input = payload.value; @@ -2884,6 +2999,13 @@ export const $ZodEnum: core.$constructor<$ZodEnum> = /*@__PURE__*/ core.$constru .map((o) => (typeof o === "string" ? util.escapeRegex(o) : o.toString())) .join("|")})$` ); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json) => { + const list = Array.from(inst._zod.values ?? []); + if (list.length) { + json.enum = list as Array; + } + }); inst._zod.parse = (payload, _ctx) => { const input = payload.value; @@ -2940,6 +3062,11 @@ export const $ZodLiteral: core.$constructor<$ZodLiteral> = /*@__PURE__*/ core.$c .map((o) => (typeof o === "string" ? util.escapeRegex(o) : o ? util.escapeRegex(o.toString()) : String(o))) .join("|")})$` ); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json) => { + const list = Array.from(inst._zod.values) as Array; + json.enum = list; + }); inst._zod.parse = (payload, _ctx) => { const input = payload.value; @@ -3137,6 +3264,11 @@ export const $ZodOptional: core.$constructor<$ZodOptional> = /*@__PURE__*/ core. (inst, def) => { $ZodType.init(inst, def); inst._zod.optin = "optional"; + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + const inner = def.innerType._zod.getJSONSchema(context); + Object.assign(json, inner); + }); inst._zod.optout = "optional"; util.defineLazy(inst._zod, "values", () => { @@ -3191,6 +3323,11 @@ export const $ZodNullable: core.$constructor<$ZodNullable> = /*@__PURE__*/ core. "$ZodNullable", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + const inner = def.innerType._zod.getJSONSchema(context); + json.anyOf = [inner, { type: "null" }]; + }); util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); @@ -3246,6 +3383,13 @@ export const $ZodDefault: core.$constructor<$ZodDefault> = /*@__PURE__*/ core.$c // inst._zod.qin = "true"; inst._zod.optin = "optional"; + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + const inner = def.innerType._zod.getJSONSchema(context); + Object.assign(json, inner); + const value = def.defaultValue; + json.default = typeof value === "function" ? value() : value; + }); util.defineLazy(inst._zod, "values", () => def.innerType._zod.values); inst._zod.parse = (payload, ctx) => { @@ -4154,6 +4298,11 @@ export interface $ZodLazy extends $ZodType { export const $ZodLazy: core.$constructor<$ZodLazy> = /*@__PURE__*/ core.$constructor("$ZodLazy", (inst, def) => { $ZodType.init(inst, def); + inst._zod.getJSONSchema = (ctx) => + toJSON(inst, ctx, (json, context) => { + const resolved = inst._zod.innerType._zod.getJSONSchema(context); + Object.assign(json, resolved); + }); // let _innerType!: any; // util.defineLazy(def, "getter", () => { From 5d3c9e54b439693b25484b6ab48863392a487e5c Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Fri, 17 Oct 2025 13:38:47 -0700 Subject: [PATCH 3/3] WIP --- packages/zod/src/v4/core/schemas.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index b45ac9b60..21e9de610 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -8,7 +8,6 @@ import type * as JSONSchema from "./json-schema.js"; import { parse, parseAsync, safeParse, safeParseAsync } from "./parse.js"; import * as regexes from "./regexes.js"; import type { StandardSchemaV1 } from "./standard-schema.js"; -import { toJSONSchema } from "./to-json-schema.js"; import * as util from "./util.js"; import { version } from "./versions.js"; @@ -184,9 +183,6 @@ export interface _$ZodType export const $ZodType: core.$constructor<$ZodType> = /*@__PURE__*/ core.$constructor("$ZodType", (inst, def) => { inst ??= {} as any; - // @ts-ignore - inst.asdf = toJSONSchema; - inst._zod.def = def; // set _def property inst._zod.bag = inst._zod.bag || {}; // initialize _bag object inst._zod.version = version;