From c7d4003d3022c947ceda7128a60982a0f9c7654a Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Fri, 16 May 2025 23:42:33 -0700 Subject: [PATCH 1/7] Add types for bag. Fix setting of min/max in json schema --- packages/zod/src/v4/classic/schemas.ts | 25 ++-- .../zod/src/v4/classic/tests/catch.test.ts | 8 +- .../src/v4/classic/tests/json-schema.test.ts | 122 ++++++++++++++++++ packages/zod/src/v4/core/checks.ts | 77 +++++------ packages/zod/src/v4/core/regexes.ts | 2 +- packages/zod/src/v4/core/schemas.ts | 61 +++++++-- packages/zod/src/v4/core/to-json-schema.ts | 36 +++--- packages/zod/src/v4/core/util.ts | 5 +- .../zod/src/v4/mini/tests/computed.test.ts | 24 ++-- play.ts | 13 +- 10 files changed, 271 insertions(+), 102 deletions(-) diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts index a1a0d001cd..969a6842d9 100644 --- a/packages/zod/src/v4/classic/schemas.ts +++ b/packages/zod/src/v4/classic/schemas.ts @@ -251,9 +251,9 @@ export const _ZodString: core.$constructor<_ZodString> = /*@__PURE__*/ core.$con core.$ZodString.init(inst, def); ZodType.init(inst, def); - inst.format = inst._zod.computed.format ?? null; - inst.minLength = inst._zod.computed.minimum ?? null; - inst.maxLength = inst._zod.computed.maximum ?? null; + inst.format = inst._zod.bag.format ?? null; + inst.minLength = inst._zod.bag.minimum ?? null; + inst.maxLength = inst._zod.bag.maximum ?? null; // validations inst.regex = (...args) => inst.check(checks.regex(...args)); @@ -719,7 +719,7 @@ export interface _ZodNumber extends ZodType { step(value: number, params?: string | core.$ZodCheckMultipleOfParams): this; /** @deprecated In v4 and later, z.number() does not allow infinite values by default. This is a no-op. */ - finite(params?: any): this; + finite(params?: unknown): this; minValue: number | null; maxValue: number | null; @@ -754,12 +754,11 @@ export const ZodNumber: core.$constructor = /*@__PURE__*/ core.$const // inst.finite = (params) => inst.check(core.finite(params)); inst.finite = () => inst; - inst.minValue = inst._zod.computed.minimum ?? null; - inst.maxValue = inst._zod.computed.maximum ?? null; - inst.isInt = - (inst._zod.computed.format ?? "").includes("int") || Number.isSafeInteger(inst._zod.computed.multipleOf ?? 0.5); + inst.minValue = inst._zod.bag.minimum ?? null; + inst.maxValue = inst._zod.bag.maximum ?? null; + inst.isInt = (inst._zod.bag.format ?? "").includes("int") || Number.isSafeInteger(inst._zod.bag.multipleOf ?? 0.5); inst.isFinite = true; - inst.format = inst._zod.computed.format ?? null; + inst.format = inst._zod.bag.format ?? null; }); export function number(params?: string | core.$ZodNumberParams): ZodNumber { @@ -864,9 +863,9 @@ export const ZodBigInt: core.$constructor = /*@__PURE__*/ core.$const inst.nonnegative = (params) => inst.check(checks.gte(BigInt(0), params)); inst.multipleOf = (value, params) => inst.check(checks.multipleOf(value, params)); - inst.minValue = inst._zod.computed.minimum ?? null; - inst.maxValue = inst._zod.computed.maximum ?? null; - inst.format = inst._zod.computed.format ?? null; + inst.minValue = inst._zod.bag.minimum ?? null; + inst.maxValue = inst._zod.bag.maximum ?? null; + inst.format = inst._zod.bag.format ?? null; }); export function bigint(params?: string | core.$ZodBigIntParams): ZodBigInt { @@ -1014,7 +1013,7 @@ export const ZodDate: core.$constructor = /*@__PURE__*/ core.$construct inst.min = (value, params) => inst.check(checks.gte(value, params)); inst.max = (value, params) => inst.check(checks.lte(value, params)); - const c = inst._zod.computed; + const c = inst._zod.bag; inst.minDate = c.minimum ? new Date(c.minimum) : null; inst.maxDate = c.maximum ? new Date(c.maximum) : null; }); diff --git a/packages/zod/src/v4/classic/tests/catch.test.ts b/packages/zod/src/v4/classic/tests/catch.test.ts index efcfd3fca8..f613d4cdcd 100644 --- a/packages/zod/src/v4/classic/tests/catch.test.ts +++ b/packages/zod/src/v4/classic/tests/catch.test.ts @@ -45,7 +45,7 @@ test("catch with transform", () => { expect(stringWithDefault.unwrap().out).toBeInstanceOf(z.ZodTransform); type inp = z.input; - expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf(); type out = z.output; expectTypeOf().toEqualTypeOf(); }); @@ -59,7 +59,7 @@ test("catch on existing optional", () => { expect(stringWithDefault.unwrap().unwrap()).toBeInstanceOf(z.ZodString); type inp = z.input; - expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf(); type out = z.output; expectTypeOf().toEqualTypeOf(); }); @@ -68,7 +68,7 @@ test("optional on catch", () => { const stringWithDefault = z.string().catch("asdf").optional(); type inp = z.input; - expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf(); type out = z.output; expectTypeOf().toEqualTypeOf(); }); @@ -102,7 +102,7 @@ test("nested", () => { inner: "asdf", }); type input = z.input; - expectTypeOf().toEqualTypeOf }>>(); + expectTypeOf().toEqualTypeOf<{ inner: string | util.Whatever } | util.Whatever>(); type out = z.output; expectTypeOf().toEqualTypeOf<{ inner: string }>(); diff --git a/packages/zod/src/v4/classic/tests/json-schema.test.ts b/packages/zod/src/v4/classic/tests/json-schema.test.ts index 9786d94814..9d01baefbe 100644 --- a/packages/zod/src/v4/classic/tests/json-schema.test.ts +++ b/packages/zod/src/v4/classic/tests/json-schema.test.ts @@ -329,6 +329,80 @@ describe("toJSONSchema", () => { } ` ); + + expect(toJSONSchema(z.number().gt(5).gt(10))).toMatchInlineSnapshot(` + { + "exclusiveMinimum": 10, + "type": "number", + } + `); + + expect(toJSONSchema(z.number().gt(5).gte(10))).toMatchInlineSnapshot(` + { + "minimum": 10, + "type": "number", + } + `); + + expect(toJSONSchema(z.number().lt(5).lt(3))).toMatchInlineSnapshot(` + { + "exclusiveMaximum": 3, + "type": "number", + } + `); + + expect(toJSONSchema(z.number().lt(5).lt(3).lte(2))).toMatchInlineSnapshot(` + { + "maximum": 2, + "type": "number", + } + `); + + expect(toJSONSchema(z.number().lt(5).lte(3))).toMatchInlineSnapshot(` + { + "maximum": 3, + "type": "number", + } + `); + + expect(toJSONSchema(z.number().gt(5).lt(10))).toMatchInlineSnapshot(` + { + "exclusiveMaximum": 10, + "exclusiveMinimum": 5, + "type": "number", + } + `); + expect(toJSONSchema(z.number().gte(5).lte(10))).toMatchInlineSnapshot(` + { + "maximum": 10, + "minimum": 5, + "type": "number", + } + `); + expect(toJSONSchema(z.number().positive())).toMatchInlineSnapshot(` + { + "exclusiveMinimum": 0, + "type": "number", + } + `); + expect(toJSONSchema(z.number().negative())).toMatchInlineSnapshot(` + { + "exclusiveMaximum": 0, + "type": "number", + } + `); + expect(toJSONSchema(z.number().nonpositive())).toMatchInlineSnapshot(` + { + "maximum": 0, + "type": "number", + } + `); + expect(toJSONSchema(z.number().nonnegative())).toMatchInlineSnapshot(` + { + "minimum": 0, + "type": "number", + } + `); }); test("arrays", () => { @@ -1544,3 +1618,51 @@ test("override with refs", () => { } `); }); + +// test("number checks", () => { +// expect(z.toJSONSchema(z.number().int())).toMatchInlineSnapshot(` +// { +// "maximum": 9007199254740991, +// "minimum": -9007199254740991, +// "type": "integer", +// } +// `); +// expect(z.toJSONSchema(z.int())).toMatchInlineSnapshot(` +// { +// "maximum": 9007199254740991, +// "minimum": -9007199254740991, +// "type": "integer", +// } +// `); +// expect(z.toJSONSchema(z.int().positive())).toMatchInlineSnapshot(` +// { +// "exclusiveMinimum": 0, +// "maximum": 9007199254740991, +// "minimum": -9007199254740991, +// "type": "integer", +// } +// `); +// expect(z.toJSONSchema(z.int().nonnegative())).toMatchInlineSnapshot(` +// { +// "maximum": 9007199254740991, +// "minimum": 0, +// "type": "integer", +// } +// `); +// expect(z.toJSONSchema(z.int().gt(0))).toMatchInlineSnapshot(` +// { +// "exclusiveMinimum": 0, +// "maximum": 9007199254740991, +// "minimum": -9007199254740991, +// "type": "integer", +// } +// `); +// expect(z.toJSONSchema(z.int().gte(0))).toMatchInlineSnapshot(` +// { +// "maximum": 9007199254740991, +// "minimum": 0, +// "type": "integer", +// } +// `); + +// }); diff --git a/packages/zod/src/v4/core/checks.ts b/packages/zod/src/v4/core/checks.ts index 3ede95dd82..a2a68097e9 100644 --- a/packages/zod/src/v4/core/checks.ts +++ b/packages/zod/src/v4/core/checks.ts @@ -70,10 +70,11 @@ export const $ZodCheckLessThan: core.$constructor<$ZodCheckLessThan> = /*@__PURE const origin = numericOriginMap[typeof def.value as "number" | "bigint" | "object"]; inst._zod.onattach.push((inst) => { - const curr = inst._zod.computed.maximum ?? Number.POSITIVE_INFINITY; + const bag = inst._zod.bag; + const curr = (def.inclusive ? bag.maximum : bag.exclusiveMaximum) ?? Number.POSITIVE_INFINITY; if (def.value < curr) { - inst._zod.computed.maximum = def.value; - inst._zod.computed.inclusive = def.inclusive; + if (def.inclusive) bag.maximum = def.value; + else bag.exclusiveMaximum = def.value; } }); @@ -120,10 +121,11 @@ export const $ZodCheckGreaterThan: core.$constructor<$ZodCheckGreaterThan> = /*@ const origin = numericOriginMap[typeof def.value as "number" | "bigint" | "object"]; inst._zod.onattach.push((inst) => { - const curr = inst._zod.computed.minimum ?? Number.NEGATIVE_INFINITY; + const bag = inst._zod.bag; + const curr = (def.inclusive ? bag.minimum : bag.exclusiveMinimum) ?? Number.NEGATIVE_INFINITY; if (def.value > curr) { - inst._zod.computed.minimum = def.value; - inst._zod.computed.inclusive = def.inclusive; + if (def.inclusive) bag.minimum = def.value; + else bag.exclusiveMinimum = def.value; } }); @@ -170,7 +172,7 @@ export const $ZodCheckMultipleOf: core.$constructor<$ZodCheckMultipleOf { - inst._zod.computed.multipleOf ??= def.value; + inst._zod.bag.multipleOf ??= def.value; }); inst._zod.check = (payload) => { @@ -212,7 +214,7 @@ export const $ZodCheckMultipleOf: core.$constructor<$ZodCheckMultipleOf { -// inst["_computed"].finite = true; +// inst["_bag"].finite = true; // }; // inst._zod.check = (payload) => { @@ -252,6 +254,9 @@ export interface $ZodCheckNumberFormatDef extends $ZodCheckDef { export interface $ZodCheckNumberFormatInternals extends $ZodCheckInternals { def: $ZodCheckNumberFormatDef; issc: errors.$ZodIssueInvalidType | errors.$ZodIssueTooBig<"number"> | errors.$ZodIssueTooSmall<"number">; + // bag: util.LoosePartial<{ + // minimum?: number | undefined; + // }>; } export interface $ZodCheckNumberFormat extends $ZodCheck { @@ -269,11 +274,11 @@ export const $ZodCheckNumberFormat: core.$constructor<$ZodCheckNumberFormat> = / const [minimum, maximum] = util.NUMBER_FORMAT_RANGES[def.format]; inst._zod.onattach.push((inst) => { - inst._zod.computed.format = def.format; - inst._zod.computed.minimum = minimum; - inst._zod.computed.maximum = maximum; - inst._zod.computed.inclusive = true; - if (isInt) inst._zod.computed.pattern = regexes.integer; + const bag = inst._zod.bag; + bag.format = def.format; + bag.minimum = minimum; + bag.maximum = maximum; + if (isInt) bag.pattern = regexes.integer; }); inst._zod.check = (payload) => { @@ -383,9 +388,9 @@ export const $ZodCheckBigIntFormat: core.$constructor<$ZodCheckBigIntFormat> = / const [minimum, maximum] = util.BIGINT_FORMAT_RANGES[def.format!]; inst._zod.onattach.push((inst) => { - inst._zod.computed.format = def.format; - inst._zod.computed.minimum = minimum; - inst._zod.computed.maximum = maximum; + inst._zod.bag.format = def.format; + inst._zod.bag.minimum = minimum; + inst._zod.bag.maximum = maximum; }); inst._zod.check = (payload) => { @@ -444,8 +449,8 @@ export const $ZodCheckMaxSize: core.$constructor<$ZodCheckMaxSize> = /*@__PURE__ }; inst._zod.onattach.push((inst) => { - const curr = (inst._zod.computed.maximum ?? Number.POSITIVE_INFINITY) as number; - if (def.maximum < curr) inst._zod.computed.maximum = def.maximum; + const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY) as number; + if (def.maximum < curr) inst._zod.bag.maximum = def.maximum; }); inst._zod.check = (payload) => { @@ -493,8 +498,8 @@ export const $ZodCheckMinSize: core.$constructor<$ZodCheckMinSize> = /*@__PURE__ }; inst._zod.onattach.push((inst) => { - const curr = (inst._zod.computed.minimum ?? Number.NEGATIVE_INFINITY) as number; - if (def.minimum > curr) inst._zod.computed.minimum = def.minimum; + const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY) as number; + if (def.minimum > curr) inst._zod.bag.minimum = def.minimum; }); inst._zod.check = (payload) => { @@ -542,9 +547,9 @@ export const $ZodCheckSizeEquals: core.$constructor<$ZodCheckSizeEquals> = /*@__ }; inst._zod.onattach.push((inst) => { - inst._zod.computed.minimum = def.size; - inst._zod.computed.maximum = def.size; - inst._zod.computed.size = def.size; + inst._zod.bag.minimum = def.size; + inst._zod.bag.maximum = def.size; + inst._zod.bag.size = def.size; }); inst._zod.check = (payload) => { @@ -593,8 +598,8 @@ export const $ZodCheckMaxLength: core.$constructor<$ZodCheckMaxLength> = /*@__PU }; inst._zod.onattach.push((inst) => { - const curr = (inst._zod.computed.maximum ?? Number.POSITIVE_INFINITY) as number; - if (def.maximum < curr) inst._zod.computed.maximum = def.maximum; + const curr = (inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY) as number; + if (def.maximum < curr) inst._zod.bag.maximum = def.maximum; }); inst._zod.check = (payload) => { @@ -643,8 +648,8 @@ export const $ZodCheckMinLength: core.$constructor<$ZodCheckMinLength> = /*@__PU }; inst._zod.onattach.push((inst) => { - const curr = (inst._zod.computed.minimum ?? Number.NEGATIVE_INFINITY) as number; - if (def.minimum > curr) inst._zod.computed.minimum = def.minimum; + const curr = (inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY) as number; + if (def.minimum > curr) inst._zod.bag.minimum = def.minimum; }); inst._zod.check = (payload) => { @@ -694,9 +699,9 @@ export const $ZodCheckLengthEquals: core.$constructor<$ZodCheckLengthEquals> = / }; inst._zod.onattach.push((inst) => { - inst._zod.computed.minimum = def.length; - inst._zod.computed.maximum = def.length; - inst._zod.computed.length = def.length; + inst._zod.bag.minimum = def.length; + inst._zod.bag.maximum = def.length; + inst._zod.bag.length = def.length; }); inst._zod.check = (payload) => { @@ -771,8 +776,8 @@ export const $ZodCheckStringFormat: core.$constructor<$ZodCheckStringFormat> = / $ZodCheck.init(inst, def); inst._zod.onattach.push((inst) => { - inst._zod.computed.format = def.format; - if (def.pattern) inst._zod.computed.pattern = def.pattern; + inst._zod.bag.format = def.format; + if (def.pattern) inst._zod.bag.pattern = def.pattern; }); inst._zod.check ??= (payload) => { @@ -935,7 +940,7 @@ export const $ZodCheckIncludes: core.$constructor<$ZodCheckIncludes> = /*@__PURE const pattern = new RegExp(util.escapeRegex(def.includes)); def.pattern = pattern; inst._zod.onattach.push((inst) => { - inst._zod.computed.pattern = pattern; + inst._zod.bag.pattern = pattern; }); inst._zod.check = (payload) => { @@ -977,7 +982,7 @@ export const $ZodCheckStartsWith: core.$constructor<$ZodCheckStartsWith> = /*@__ const pattern = new RegExp(`^${util.escapeRegex(def.prefix)}.*`); def.pattern ??= pattern; inst._zod.onattach.push((inst) => { - inst._zod.computed.pattern = pattern; + inst._zod.bag.pattern = pattern; }); inst._zod.check = (payload) => { @@ -1019,7 +1024,7 @@ export const $ZodCheckEndsWith: core.$constructor<$ZodCheckEndsWith> = /*@__PURE const pattern = new RegExp(`.*${util.escapeRegex(def.suffix)}$`); def.pattern ??= pattern; inst._zod.onattach.push((inst) => { - inst._zod.computed.pattern = new RegExp(`.*${util.escapeRegex(def.suffix)}$`); + inst._zod.bag.pattern = new RegExp(`.*${util.escapeRegex(def.suffix)}$`); }); inst._zod.check = (payload) => { @@ -1111,7 +1116,7 @@ export const $ZodCheckMimeType: core.$constructor<$ZodCheckMimeType> = /*@__PURE $ZodCheck.init(inst, def); const mimeSet = new Set(def.mime); inst._zod.onattach.push((inst) => { - inst._zod.computed.mime = def.mime; + inst._zod.bag.mime = def.mime; }); inst._zod.check = (payload) => { if (mimeSet.has(payload.value.type)) return; diff --git a/packages/zod/src/v4/core/regexes.ts b/packages/zod/src/v4/core/regexes.ts index e6a3799463..62490d7f9a 100644 --- a/packages/zod/src/v4/core/regexes.ts +++ b/packages/zod/src/v4/core/regexes.ts @@ -113,7 +113,7 @@ export function datetime(args: { return new RegExp(`^${regex}$`); } -export const string = (params?: { minimum?: number; maximum?: number }): RegExp => { +export const string = (params?: { minimum?: number | undefined; maximum?: number | undefined }): RegExp => { const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`; return new RegExp(`^${regex}$`); }; diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index 34acf032d9..46181c5ead 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -138,8 +138,22 @@ export interface $ZodTypeInternals { def: any ) => $ZodType; - /** A catchall object for computed metadata related to this schema. Commonly modified by checks using `onattach`. */ - computed: Record; + /** A catchall object for bag metadata related to this schema. Commonly modified by checks using `onattach`. */ + // bag: Record; + bag: util.LoosePartial<{ + minimum: util.Numeric; + maximum: util.Numeric; + multipleOf: util.Numeric; + exclusiveMinimum: util.Numeric; + exclusiveMaximum: util.Numeric; + pattern: RegExp; + format: string; + contentEncoding: string; + contentType: string; + mime: util.MimeTypes[]; + size: number; + length: number; + }>; /** The set of issues this schema might throw during type checking. */ isst: errors.$ZodIssueBase; @@ -171,7 +185,7 @@ export const $ZodType: core.$constructor<$ZodType> = /*@__PURE__*/ core.$constru inst ??= {} as any; inst._zod.id = def.type + "_" + util.randomString(10); inst._zod.def = def; // set _def property - inst._zod.computed = inst._zod.computed || {}; // initialize _computed object + inst._zod.bag = inst._zod.bag || {}; // initialize _bag object inst._zod.version = version; const checks = [...(inst._zod.def.checks ?? [])]; @@ -289,6 +303,12 @@ export interface $ZodStringInternals extends $ZodTypeInternals; } export interface $ZodString extends $ZodType { @@ -297,7 +317,7 @@ export interface $ZodString extends $ZodType { export const $ZodString: core.$constructor<$ZodString> = /*@__PURE__*/ core.$constructor("$ZodString", (inst, def) => { $ZodType.init(inst, def); - inst._zod.pattern = inst?._zod.computed?.pattern ?? regexes.string(inst._zod.computed); + inst._zod.pattern = inst?._zod.bag?.pattern ?? regexes.string(inst._zod.bag); inst._zod.parse = (payload, _) => { if (def.coerce) try { @@ -684,7 +704,7 @@ export const $ZodIPv4: core.$constructor<$ZodIPv4> = /*@__PURE__*/ core.$constru def.pattern ??= regexes.ipv4; $ZodStringFormat.init(inst, def); inst._zod.onattach.push((inst) => { - inst._zod.computed.format = `ipv4`; + inst._zod.bag.format = `ipv4`; }); }); @@ -707,7 +727,7 @@ export const $ZodIPv6: core.$constructor<$ZodIPv6> = /*@__PURE__*/ core.$constru $ZodStringFormat.init(inst, def); inst._zod.onattach.push((inst) => { - inst._zod.computed.format = `ipv6`; + inst._zod.bag.format = `ipv6`; }); inst._zod.check = (payload) => { @@ -835,7 +855,7 @@ export const $ZodBase64: core.$constructor<$ZodBase64> = /*@__PURE__*/ core.$con $ZodStringFormat.init(inst, def); inst._zod.onattach.push((inst) => { - inst._zod.computed.contentEncoding = "base64"; + inst._zod.bag.contentEncoding = "base64"; }); inst._zod.check = (payload) => { @@ -873,7 +893,7 @@ export const $ZodBase64URL: core.$constructor<$ZodBase64URL> = /*@__PURE__*/ cor $ZodStringFormat.init(inst, def); inst._zod.onattach.push((inst) => { - inst._zod.computed.contentEncoding = "base64url"; + inst._zod.bag.contentEncoding = "base64url"; }); inst._zod.check = (payload) => { @@ -993,6 +1013,12 @@ export interface $ZodNumberInternals extends $ZodTypeInternals< pattern: RegExp; /** @deprecated Internal API, use with caution (not deprecated) */ isst: errors.$ZodIssueInvalidType; + bag: util.LoosePartial<{ + minimum: number; + maximum: number; + format: string; + pattern: RegExp; + }>; } export interface $ZodNumber extends $ZodType { @@ -1001,7 +1027,7 @@ 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.computed.pattern ?? regexes.number; + inst._zod.pattern = inst._zod.bag.pattern ?? regexes.number; inst._zod.parse = (payload, _ctx) => { if (def.coerce) @@ -1122,6 +1148,11 @@ export interface $ZodBigIntInternals extends $ZodTypeInternals; } export interface $ZodBigInt extends $ZodType { @@ -1436,6 +1467,11 @@ export interface $ZodDateDef extends $ZodTypeDef { export interface $ZodDateInternals extends $ZodTypeInternals { def: $ZodDateDef; isst: errors.$ZodIssueInvalidType; // | errors.$ZodIssueInvalidDate; + bag: util.LoosePartial<{ + minimum: Date; + maximum: Date; + format: string; + }>; } export interface $ZodDate extends $ZodType { @@ -1939,7 +1975,7 @@ export const $ZodUnion: core.$constructor<$ZodUnion> = /*@__PURE__*/ core.$const inst._zod.values = values; } - // computed union regex for pattern if all options have pattern + // bag union regex for pattern if all options have pattern if (def.options.every((o) => o._zod.pattern)) { const patterns = def.options.map((o) => o._zod.pattern); inst._zod.pattern = new RegExp(`^(${patterns.map((p) => util.cleanRegex(p!.source)).join("|")})$`); @@ -3286,7 +3322,7 @@ export interface $ZodCatchDef extends $ZodTypeDef { } export interface $ZodCatchInternals - extends $ZodTypeInternals, util.Loose>> { + extends $ZodTypeInternals, core.input | util.Whatever> { def: $ZodCatchDef; // qin: T["_zod"]["qin"]; // qout: T["_zod"]["qout"]; @@ -3674,6 +3710,9 @@ export interface $ZodCustomInternals def: $ZodCustomDef; issc: errors.$ZodIssue; isst: never; + bag: util.LoosePartial<{ + Class: typeof util.Class; + }>; } export interface $ZodCustom extends $ZodType { diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts index 97482c998d..920b06c694 100644 --- a/packages/zod/src/v4/core/to-json-schema.ts +++ b/packages/zod/src/v4/core/to-json-schema.ts @@ -141,7 +141,7 @@ export class JSONSchemaGenerator { case "string": { const json: JSONSchema.StringSchema = _json as any; json.type = "string"; - const { minimum, maximum, format, pattern, contentEncoding } = schema._zod.computed as { + const { minimum, maximum, format, pattern, contentEncoding } = schema._zod.bag as { minimum?: number; maximum?: number; format?: checks.$ZodStringFormats; @@ -163,24 +163,28 @@ export class JSONSchemaGenerator { } case "number": { const json: JSONSchema.NumberSchema | JSONSchema.IntegerSchema = _json as any; - const { minimum, maximum, format, multipleOf, inclusive } = schema._zod.computed as { - minimum?: number; - maximum?: number; - format?: checks.$ZodNumberFormats; - multipleOf?: number; - inclusive?: boolean; - }; + const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag; if (format?.includes("int")) json.type = "integer"; else json.type = "number"; + if (typeof exclusiveMinimum === "number") json.exclusiveMinimum = exclusiveMinimum; if (typeof minimum === "number") { - if (inclusive) json.minimum = minimum; - else json.exclusiveMinimum = minimum; + json.minimum = minimum; + if (typeof exclusiveMinimum === "number") { + if (exclusiveMinimum >= minimum) delete json.minimum; + else delete json.exclusiveMinimum; + } } + + if (typeof exclusiveMaximum === "number") json.exclusiveMaximum = exclusiveMaximum; if (typeof maximum === "number") { - if (inclusive) json.maximum = maximum; - else json.exclusiveMaximum = maximum; + json.maximum = maximum; + if (typeof exclusiveMaximum === "number") { + if (exclusiveMaximum <= maximum) delete json.maximum; + else delete json.exclusiveMaximum; + } } + if (typeof multipleOf === "number") json.multipleOf = multipleOf; break; @@ -235,12 +239,10 @@ export class JSONSchemaGenerator { } case "array": { const json: JSONSchema.ArraySchema = _json as any; - const { minimum, maximum } = schema._zod.computed as { - minimum?: number; - maximum?: number; - }; + const { minimum, maximum } = schema._zod.bag; if (typeof minimum === "number") json.minItems = minimum; if (typeof maximum === "number") json.maxItems = maximum; + json.type = "array"; json.items = this.process(def.element, { ...params, path: [...params.path, "items"] }); break; @@ -342,7 +344,7 @@ export class JSONSchemaGenerator { } // length - const { minimum, maximum } = schema._zod.computed as { + const { minimum, maximum } = schema._zod.bag as { minimum?: number; maximum?: number; }; diff --git a/packages/zod/src/v4/core/util.ts b/packages/zod/src/v4/core/util.ts index affb0cd1bb..1cbcc53da9 100644 --- a/packages/zod/src/v4/core/util.ts +++ b/packages/zod/src/v4/core/util.ts @@ -87,7 +87,10 @@ export type MakeRequired = Omit & Required = T & Record, never>; export type NoUndefined = T extends undefined ? never : T; -export type Loose = T | {} | undefined | null; +export type Whatever = {} | undefined | null; +export type LoosePartial = InexactPartial & { + [k: string]: unknown; +}; export type Mask = { [K in Keys]?: true }; export type Writeable = { -readonly [P in keyof T]: T[P] } & {}; export type InexactPartial = { diff --git a/packages/zod/src/v4/mini/tests/computed.test.ts b/packages/zod/src/v4/mini/tests/computed.test.ts index f58aec4638..fed0c3a696 100644 --- a/packages/zod/src/v4/mini/tests/computed.test.ts +++ b/packages/zod/src/v4/mini/tests/computed.test.ts @@ -5,32 +5,32 @@ import { util as zc } from "zod/v4/core"; test("min/max", () => { const a = z.number().check(z.minimum(5), z.minimum(6), z.minimum(7), z.maximum(10), z.maximum(11), z.maximum(12)); - expect(a._zod.computed.minimum).toEqual(7); - expect(a._zod.computed.maximum).toEqual(10); + expect(a._zod.bag.minimum).toEqual(7); + expect(a._zod.bag.maximum).toEqual(10); }); test("multipleOf", () => { const b = z.number().check(z.multipleOf(5)); - expect(b._zod.computed.multipleOf).toEqual(5); + expect(b._zod.bag.multipleOf).toEqual(5); }); test("int64 format", () => { const c = z.int64(); - expect(c._zod.computed.format).toEqual("int64"); - expect(c._zod.computed.minimum).toEqual(zc.BIGINT_FORMAT_RANGES.int64[0]); - expect(c._zod.computed.maximum).toEqual(zc.BIGINT_FORMAT_RANGES.int64[1]); + expect(c._zod.bag.format).toEqual("int64"); + expect(c._zod.bag.minimum).toEqual(zc.BIGINT_FORMAT_RANGES.int64[0]); + expect(c._zod.bag.maximum).toEqual(zc.BIGINT_FORMAT_RANGES.int64[1]); }); test("int32 format", () => { const d = z.int32(); - expect(d._zod.computed.format).toEqual("int32"); - expect(d._zod.computed.minimum).toEqual(zc.NUMBER_FORMAT_RANGES.int32[0]); - expect(d._zod.computed.maximum).toEqual(zc.NUMBER_FORMAT_RANGES.int32[1]); + expect(d._zod.bag.format).toEqual("int32"); + expect(d._zod.bag.minimum).toEqual(zc.NUMBER_FORMAT_RANGES.int32[0]); + expect(d._zod.bag.maximum).toEqual(zc.NUMBER_FORMAT_RANGES.int32[1]); }); test("array size", () => { const e = z.array(z.string()).check(z.length(5)); - expect(e._zod.computed.length).toEqual(5); - expect(e._zod.computed.minimum).toEqual(5); - expect(e._zod.computed.maximum).toEqual(5); + expect(e._zod.bag.length).toEqual(5); + expect(e._zod.bag.minimum).toEqual(5); + expect(e._zod.bag.maximum).toEqual(5); }); diff --git a/play.ts b/play.ts index 1263049de4..6ccefc1b07 100644 --- a/play.ts +++ b/play.ts @@ -2,11 +2,10 @@ import { z } from "zod/v4"; z; -const stringWithDefault = z - .pipe( - z.transform((v) => (v === "none" ? undefined : v)), - z.string() - ) - .catch("default"); +const schemas = [z.number().int(), z.int(), z.int().positive(), z.int().nonnegative(), z.int().gt(0), z.int().gte(0)]; -stringWithDefault.parse(undefined); +const a = z.int(); +console.dir(z.toJSONSchema(a), { depth: null }); +const b = z.int().positive(); +console.dir(b._zod.bag, { depth: null }); +console.dir(z.toJSONSchema(b), { depth: null }); From 83689f2673ce1f29c9b13de1e49991c5334e33f9 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Fri, 16 May 2025 23:48:29 -0700 Subject: [PATCH 2/7] Fix minValue/maxValue --- packages/zod/src/v3/tests/number.test.ts | 8 ++++---- packages/zod/src/v4/classic/schemas.ts | 11 +++++++---- packages/zod/src/v4/core/schemas.ts | 2 ++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/zod/src/v3/tests/number.test.ts b/packages/zod/src/v3/tests/number.test.ts index 1fd9be66ee..db3f73b2df 100644 --- a/packages/zod/src/v3/tests/number.test.ts +++ b/packages/zod/src/v3/tests/number.test.ts @@ -4,11 +4,11 @@ import { expect, test } from "vitest"; import * as z from "zod/v3"; const gtFive = z.number().gt(5); -const gteFive = z.number().gte(5); -const minFive = z.number().min(5); -const ltFive = z.number().lt(5); +const gteFive = z.number().gte(-5).gte(5); +const minFive = z.number().min(0).min(5); +const ltFive = z.number().lte(10).lt(5); const lteFive = z.number().lte(5); -const maxFive = z.number().max(5); +const maxFive = z.number().max(10).max(5); const intNum = z.number().int(); const positive = z.number().positive(); const negative = z.number().negative(); diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts index 969a6842d9..4091703c3c 100644 --- a/packages/zod/src/v4/classic/schemas.ts +++ b/packages/zod/src/v4/classic/schemas.ts @@ -754,11 +754,14 @@ export const ZodNumber: core.$constructor = /*@__PURE__*/ core.$const // inst.finite = (params) => inst.check(core.finite(params)); inst.finite = () => inst; - inst.minValue = inst._zod.bag.minimum ?? null; - inst.maxValue = inst._zod.bag.maximum ?? null; - inst.isInt = (inst._zod.bag.format ?? "").includes("int") || Number.isSafeInteger(inst._zod.bag.multipleOf ?? 0.5); + const bag = inst._zod.bag; + inst.minValue = + Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null; + inst.maxValue = + Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null; + inst.isInt = (bag.format ?? "").includes("int") || Number.isSafeInteger(bag.multipleOf ?? 0.5); inst.isFinite = true; - inst.format = inst._zod.bag.format ?? null; + inst.format = bag.format ?? null; }); export function number(params?: string | core.$ZodNumberParams): ZodNumber { diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index 46181c5ead..545138b387 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -1016,6 +1016,8 @@ export interface $ZodNumberInternals extends $ZodTypeInternals< bag: util.LoosePartial<{ minimum: number; maximum: number; + exclusiveMinimum: number; + exclusiveMaximum: number; format: string; pattern: RegExp; }>; From 8165d1652f4a8c1391deb7d7196a47f6a196d948 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Fri, 16 May 2025 23:54:17 -0700 Subject: [PATCH 3/7] Improve number tests --- packages/zod/src/v4/classic/tests/number.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/zod/src/v4/classic/tests/number.test.ts b/packages/zod/src/v4/classic/tests/number.test.ts index 51262a7d8e..1e9fda71f0 100644 --- a/packages/zod/src/v4/classic/tests/number.test.ts +++ b/packages/zod/src/v4/classic/tests/number.test.ts @@ -49,37 +49,37 @@ test("Infinity validation", () => { }); test(".gt() validation", () => { - const schema = z.number().gt(5); + const schema = z.number().gt(0).gt(5); expect(schema.parse(6)).toEqual(6); expect(() => schema.parse(5)).toThrow(); }); test(".gte() validation", () => { - const schema = z.number().gte(5); + const schema = z.number().gt(0).gte(1).gte(5); expect(schema.parse(5)).toEqual(5); expect(() => schema.parse(4)).toThrow(); }); test(".min() validation", () => { - const schema = z.number().min(5); + const schema = z.number().min(0).min(5); expect(schema.parse(5)).toEqual(5); expect(() => schema.parse(4)).toThrow(); }); test(".lt() validation", () => { - const schema = z.number().lt(5); + const schema = z.number().lte(10).lt(5); expect(schema.parse(4)).toEqual(4); expect(() => schema.parse(5)).toThrow(); }); test(".lte() validation", () => { - const schema = z.number().lte(5); + const schema = z.number().lte(10).lte(5); expect(schema.parse(5)).toEqual(5); expect(() => schema.parse(6)).toThrow(); }); test(".max() validation", () => { - const schema = z.number().max(5); + const schema = z.number().max(10).max(5); expect(schema.parse(5)).toEqual(5); expect(() => schema.parse(6)).toThrow(); }); From 9ca1abaac5a123d53171b0f56ec4dafe4bab0509 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Fri, 16 May 2025 23:56:08 -0700 Subject: [PATCH 4/7] Trim --- packages/zod/src/v4/classic/schemas.ts | 14 ++++++++------ packages/zod/src/v4/core/checks.ts | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts index 4091703c3c..21e0fadd2f 100644 --- a/packages/zod/src/v4/classic/schemas.ts +++ b/packages/zod/src/v4/classic/schemas.ts @@ -251,9 +251,10 @@ export const _ZodString: core.$constructor<_ZodString> = /*@__PURE__*/ core.$con core.$ZodString.init(inst, def); ZodType.init(inst, def); - inst.format = inst._zod.bag.format ?? null; - inst.minLength = inst._zod.bag.minimum ?? null; - inst.maxLength = inst._zod.bag.maximum ?? null; + const bag = inst._zod.bag; + inst.format = bag.format ?? null; + inst.minLength = bag.minimum ?? null; + inst.maxLength = bag.maximum ?? null; // validations inst.regex = (...args) => inst.check(checks.regex(...args)); @@ -866,9 +867,10 @@ export const ZodBigInt: core.$constructor = /*@__PURE__*/ core.$const inst.nonnegative = (params) => inst.check(checks.gte(BigInt(0), params)); inst.multipleOf = (value, params) => inst.check(checks.multipleOf(value, params)); - inst.minValue = inst._zod.bag.minimum ?? null; - inst.maxValue = inst._zod.bag.maximum ?? null; - inst.format = inst._zod.bag.format ?? null; + const bag = inst._zod.bag; + inst.minValue = bag.minimum ?? null; + inst.maxValue = bag.maximum ?? null; + inst.format = bag.format ?? null; }); export function bigint(params?: string | core.$ZodBigIntParams): ZodBigInt { diff --git a/packages/zod/src/v4/core/checks.ts b/packages/zod/src/v4/core/checks.ts index a2a68097e9..5202d58ee0 100644 --- a/packages/zod/src/v4/core/checks.ts +++ b/packages/zod/src/v4/core/checks.ts @@ -388,9 +388,10 @@ export const $ZodCheckBigIntFormat: core.$constructor<$ZodCheckBigIntFormat> = / const [minimum, maximum] = util.BIGINT_FORMAT_RANGES[def.format!]; inst._zod.onattach.push((inst) => { - inst._zod.bag.format = def.format; - inst._zod.bag.minimum = minimum; - inst._zod.bag.maximum = maximum; + const bag = inst._zod.bag; + bag.format = def.format; + bag.minimum = minimum; + bag.maximum = maximum; }); inst._zod.check = (payload) => { @@ -547,9 +548,10 @@ export const $ZodCheckSizeEquals: core.$constructor<$ZodCheckSizeEquals> = /*@__ }; inst._zod.onattach.push((inst) => { - inst._zod.bag.minimum = def.size; - inst._zod.bag.maximum = def.size; - inst._zod.bag.size = def.size; + const bag = inst._zod.bag; + bag.minimum = def.size; + bag.maximum = def.size; + bag.size = def.size; }); inst._zod.check = (payload) => { @@ -699,9 +701,10 @@ export const $ZodCheckLengthEquals: core.$constructor<$ZodCheckLengthEquals> = / }; inst._zod.onattach.push((inst) => { - inst._zod.bag.minimum = def.length; - inst._zod.bag.maximum = def.length; - inst._zod.bag.length = def.length; + const bag = inst._zod.bag; + bag.minimum = def.length; + bag.maximum = def.length; + bag.length = def.length; }); inst._zod.check = (payload) => { From ebb9d8fb979915ced741fed9a92120a22f473d99 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Fri, 16 May 2025 23:56:45 -0700 Subject: [PATCH 5/7] Remove type on base class --- packages/zod/src/v4/core/schemas.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index 545138b387..fb16005cf1 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -139,21 +139,7 @@ export interface $ZodTypeInternals { ) => $ZodType; /** A catchall object for bag metadata related to this schema. Commonly modified by checks using `onattach`. */ - // bag: Record; - bag: util.LoosePartial<{ - minimum: util.Numeric; - maximum: util.Numeric; - multipleOf: util.Numeric; - exclusiveMinimum: util.Numeric; - exclusiveMaximum: util.Numeric; - pattern: RegExp; - format: string; - contentEncoding: string; - contentType: string; - mime: util.MimeTypes[]; - size: number; - length: number; - }>; + bag: Record; /** The set of issues this schema might throw during type checking. */ isst: errors.$ZodIssueBase; From 9ac5be2ae42a0f246f0f63b39715382aeabd11c0 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Fri, 16 May 2025 23:58:14 -0700 Subject: [PATCH 6/7] Fix err --- packages/zod/src/v4/core/to-json-schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts index 920b06c694..44a18be825 100644 --- a/packages/zod/src/v4/core/to-json-schema.ts +++ b/packages/zod/src/v4/core/to-json-schema.ts @@ -164,7 +164,7 @@ export class JSONSchemaGenerator { case "number": { const json: JSONSchema.NumberSchema | JSONSchema.IntegerSchema = _json as any; const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag; - if (format?.includes("int")) json.type = "integer"; + if (typeof format === "string" && format.includes("int")) json.type = "integer"; else json.type = "number"; if (typeof exclusiveMinimum === "number") json.exclusiveMinimum = exclusiveMinimum; From 24aa42d0ff8e5945719b0694053c4658ae87c989 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Sat, 17 May 2025 00:00:40 -0700 Subject: [PATCH 7/7] Mark internals --- packages/zod/src/v4/core/schemas.ts | 37 +++++++++++++---------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index fb16005cf1..d98e0dff3e 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -82,47 +82,44 @@ export interface $ZodTypeDef { } // @ts-ignore +/** @internal */ export interface $ZodTypeInternals { /** The `@zod/core` version of this schema */ version: typeof version; - /** Schema internals. */ + /** Schema definition. */ def: $ZodTypeDef; // types: Types; - /** Randomly generated ID for this schema. */ + /** @internal Randomly generated ID for this schema. */ id: string; - /** The inferred output type */ - // " - // "?: any; - // "~input"?: any; - // "~types"?: any; + /** @internal The inferred output type */ output: O; //extends { $out: infer O } ? O : Out; - /** The inferred input type */ + /** @internal The inferred input type */ input: I; //extends { $in: infer I } ? I : In; - /** List of deferred initializers. */ + /** @internal List of deferred initializers. */ deferred: util.AnyFunc[] | undefined; - /** Parses input and runs all checks (refinements). */ + /** @internal Parses input and runs all checks (refinements). */ run(payload: ParsePayload, ctx: ParseContextInternal): util.MaybeAsync; - /** Parses input, doesn't run checks. */ + /** @internal Parses input, doesn't run checks. */ parse(payload: ParsePayload, ctx: ParseContextInternal): util.MaybeAsync; - /** Stores identifiers for the set of traits implemented by this schema. */ + /** @internal Stores identifiers for the set of traits implemented by this schema. */ traits: Set; - /** Indicates that a schema output type should be considered optional inside objects. + /** @internal Indicates that a schema output type should be considered optional inside objects. * @default Required */ optionality?: "optional" | "defaulted" | undefined; - /** A set of literal discriminators used for the fast path in discriminated unions. */ + /** @internal A set of literal discriminators used for the fast path in discriminated unions. */ disc: util.DiscriminatorMap | undefined; - /** The set of literal values that will pass validation. Must be an exhaustive set. Used to determine optionality in z.record(). + /** @internal The set of literal values that will pass validation. Must be an exhaustive set. Used to determine optionality in z.record(). * * Defined on: enum, const, literal, null, undefined * Passthrough: optional, nullable, branded, default, catch, pipe @@ -130,24 +127,24 @@ export interface $ZodTypeInternals { */ values: util.PrimitiveSet | undefined; - /** This flag indicates that a schema validation can be represented with a regular expression. Used to determine allowable schemas in z.templateLiteral(). */ + /** @internal This flag indicates that a schema validation can be represented with a regular expression. Used to determine allowable schemas in z.templateLiteral(). */ pattern: RegExp | undefined; - /** The constructor function of this schema. */ + /** @internal The constructor function of this schema. */ constr: new ( def: any ) => $ZodType; - /** A catchall object for bag metadata related to this schema. Commonly modified by checks using `onattach`. */ + /** @internal A catchall object for bag metadata related to this schema. Commonly modified by checks using `onattach`. */ bag: Record; - /** The set of issues this schema might throw during type checking. */ + /** @internal The set of issues this schema might throw during type checking. */ isst: errors.$ZodIssueBase; /** An optional method used to override `toJSONSchema` logic. */ toJSONSchema?: () => object; - /** The parent of this schema. Only set during certain clone operations. */ + /** @internal The parent of this schema. Only set during certain clone operations. */ parent?: $ZodType | undefined; }