Repository: colinhacks/zod
Pull request #4591 (2025-06-01 08:31:02)

fix(v4): reflect inclusive boundaries in minLength/maxLength issue objects
This updates the `minLength` and `maxLength` errors in v4 to include the `inclusive` property. Currently, I believe validations like `z.string().max()` use inclusive boundaries (https://github.com/colinhacks/zod/blob/50e9afb5606018ecacd6c64aee90f4f8006d851b/packages/zod/src/v4/core/checks.ts#L619), but the default error messages do not seem to reflect this behavior.
In v3, validations like `z.string().max()` also used inclusive boundaries, and the issue objects correctly indicated this. 

If this change was intentional as part of the migration to v4, or if I have misunderstood the current behavior, please feel free to close this pull request!



Patch:
diff --git a/packages/docs/components/ecosystem-v3.tsx b/packages/docs/components/ecosystem-v3.tsx
index 32da01b9..a640a3a3 100644
--- a/packages/docs/components/ecosystem-v3.tsx
+++ b/packages/docs/components/ecosystem-v3.tsx
@@ -195,8 +195,8 @@ const formIntegrations: ZodResource[] = [
     slug: "schalkventer/frrm",
   },
   {
-    name: "@regle/schemas",
-    url: "https://github.com/victorgarciaesgi/regle/tree/main/packages/schemas",
+    name: "regle",
+    url: "https://github.com/victorgarciaesgi/regle",
     description: "Headless form validation library for Vue.js.",
     slug: "victorgarciaesgi/regle",
     v4: true,
diff --git a/packages/docs/components/ecosystem.tsx b/packages/docs/components/ecosystem.tsx
index aad67859..649edc10 100644
--- a/packages/docs/components/ecosystem.tsx
+++ b/packages/docs/components/ecosystem.tsx
@@ -49,8 +49,8 @@ const apiLibraries: ZodResource[] = [
 
 const formIntegrations: ZodResource[] = [
   {
-    name: "@regle/schemas",
-    url: "https://github.com/victorgarciaesgi/regle/tree/main/packages/schemas",
+    name: "regle",
+    url: "https://github.com/victorgarciaesgi/regle",
     description: "Headless form validation library for Vue.js.",
     slug: "victorgarciaesgi/regle",
   },
diff --git a/packages/docs/content/api.mdx b/packages/docs/content/api.mdx
index 651c0efa..6fc5e411 100644
--- a/packages/docs/content/api.mdx
+++ b/packages/docs/content/api.mdx
@@ -52,9 +52,9 @@ schema.parse(null);      // => "null"
 
   | Zod API                  | Coercion                   |
   |--------------------------|----------------------------|
-  | `z.coerce.string()`      | `new String(value)`        |
-  | `z.coerce.number()`      | `new Number(value)`        |
-  | `z.coerce.boolean()`     | `new Boolean(value)`       |
+  | `z.coerce.string()`      | `String(value)`            |
+  | `z.coerce.number()`      | `Number(value)`            |
+  | `z.coerce.boolean()`     | `Boolean(value)`           |
   | `z.coerce.bigint()`      | `BigInt(value)`            |
   | `z.coerce.date()`        | `new Date(value)`          |
 
@@ -89,7 +89,6 @@ const tuna = z.literal("tuna");
 const twelve = z.literal(12);
 const twobig = z.literal(2n);
 const tru = z.literal(true);
-const terrific = z.literal(Symbol("terrific"));
 ```
 
 To represent the JavaScript literals `null` and `undefined`:
@@ -308,7 +307,7 @@ schema.parse("mailto:noreply@zod.dev"); // ✅
 schema.parse("sup"); // ✅
 ```
 
-As you can see this is quite permissive. Internally this uses the `new URL()` constructor to valid inputs; this behavior may differ across platforms and runtimes but it's the mostly rigorous way to validate URIs/URLs on any given JS runtime/engine. 
+As you can see this is quite permissive. Internally this uses the `new URL()` constructor to validate inputs; this behavior may differ across platforms and runtimes but it's the mostly rigorous way to validate URIs/URLs on any given JS runtime/engine. 
 
 To validate the hostname against a specific regex:
 
@@ -718,7 +717,7 @@ strbool.parse("1")            // => true
 strbool.parse("yes")          // => true
 strbool.parse("on")           // => true
 strbool.parse("y")            // => true
-strbool.parse("enable")       // => true
+strbool.parse("enabled")      // => true
 
 strbool.parse("false");       // => false
 strbool.parse("0");           // => false
@@ -861,7 +860,7 @@ const hello = z.templateLiteral(["hello, ", z.string()]);
 // `hello, ${string}`
 
 const cssUnits = z.enum(["px", "em", "rem", "%"]);
-const css = z.templateLiteral([z.number(), cssUnits ]);
+const css = z.templateLiteral([z.number(), cssUnits]);
 // `${number}px` | `${number}em` | `${number}rem` | `${number}%`
 
 const email = z.templateLiteral([
@@ -950,7 +949,7 @@ Dog.parse({ name: "Yeller", extraKey: true });
 
 To define a *catchall schema* that will be used to validate any unrecognized keys:
 
-```
+```ts z.object
 const DogWithStrings = z.object({
   name: z.string(),
   age: z.number().optional(),
@@ -1507,10 +1506,10 @@ type c = z.infer<typeof c>; // => number
 This can be useful for intersecting two object types. 
 
 ```ts
-const Person = z.intersection({ name: z.string() });
+const Person = z.object({ name: z.string() });
 type Person = z.infer<typeof Person>;
 
-const Employee = z.intersection({ role: z.string() });
+const Employee = z.object({ role: z.string() });
 type Employee = z.infer<typeof Employee>;
 
 const EmployedPerson = z.intersection(Person, Employee);
@@ -1766,8 +1765,8 @@ By default, validation issues from checks are considered *continuable*; that is,
 <Tab value="Zod">
 ```ts
 const myString = z.string()
-  .refine((val) => val.length > 8)
-  .refine((val) => val === val.toLowerCase());
+  .refine((val) => val.length > 8, { error: "Too short!" })
+  .refine((val) => val === val.toLowerCase(), { error: "Must be lowercase" });
   
 
 const result = myString.safeParse("OH NO");
@@ -1781,8 +1780,8 @@ result.error.issues;
 <Tab value="Zod Mini">
 ```ts zod/v4-mini
 const myString = z.string().check(
-  z.refine((val) => val.length > 8),
-  z.refine((val) => val === val.toLowerCase())
+  z.refine((val) => val.length > 8, { error: "Too short!" }),
+  z.refine((val) => val === val.toLowerCase(), { error: "Must be lowercase" })
 );
 
 const result = z.safeParse(myString, "OH NO");
@@ -1801,8 +1800,8 @@ To mark a particular refinement as *non-continuable*, use the `abort` parameter.
 <Tab value="Zod">
 ```ts
 const myString = z.string()
-  .refine((val) => val.length > 8, { abort: true })
-  .refine((val) => val === val.toLowerCase());
+  .refine((val) => val.length > 8, { error: "Too short!", abort: true })
+  .refine((val) => val === val.toLowerCase(), { error: "Must be lowercase", abort: true });
 
 
 const result = myString.safeParse("OH NO");
@@ -1813,8 +1812,8 @@ result.error!.issues;
 <Tab value="Zod Mini">
 ```ts zod/v4-mini
 const myString = z.string().check(
-  z.refine((val) => val.length > 8, { abort: true }),
-  z.refine((val) => val === val.toLowerCase(), { abort: true })
+  z.refine((val) => val.length > 8, { error: "Too short!", abort: true }),
+  z.refine((val) => val === val.toLowerCase(), { error: "Must be lowercase", abort: true })
 );
 
 const result = z.safeParse(myString, "OH NO");
@@ -2441,7 +2440,7 @@ The `z.templateLiteral` API can handle any number of string literals (e.g. `"hel
 z.templateLiteral([ "hi there" ]); 
 // `hi there`
 
-z.templateLiteral([ "email: ", z.string()]); 
+z.templateLiteral([ "email: ", z.string() ]); 
 // `email: ${string}`
 
 z.templateLiteral([ "high", z.literal(5) ]); 
@@ -2524,7 +2523,7 @@ Function schemas have an `.implement()` method which accepts a function and retu
 
 ```ts
 const computeTrimmedLength = MyFunction.implement((input) => {
-  // TypeScript knows x is a string!
+  // TypeScript knows input is a string!
   return input.trim().length;
 });
 
diff --git a/packages/docs/content/v4/index.mdx b/packages/docs/content/v4/index.mdx
index bd318149..f47b6de4 100644
--- a/packages/docs/content/v4/index.mdx
+++ b/packages/docs/content/v4/index.mdx
@@ -750,7 +750,7 @@ strbool.parse("1")            // => true
 strbool.parse("yes")          // => true
 strbool.parse("on")           // => true
 strbool.parse("y")            // => true
-strbool.parse("enable")       // => true
+strbool.parse("enabled")      // => true
 
 strbool.parse("false");       // => false
 strbool.parse("0");           // => false
diff --git a/packages/zod/package.json b/packages/zod/package.json
index bdf658b2..0dc9ab5d 100644
--- a/packages/zod/package.json
+++ b/packages/zod/package.json
@@ -1,6 +1,6 @@
 {
   "name": "zod",
-  "version": "3.25.46",
+  "version": "3.25.49",
   "type": "module",
   "author": "Colin McDonnell <zod@colinhacks.com>",
   "description": "TypeScript-first schema declaration and validation library with static type inference",
diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts
index 84711961..9f296591 100644
--- a/packages/zod/src/v4/classic/schemas.ts
+++ b/packages/zod/src/v4/classic/schemas.ts
@@ -1955,7 +1955,7 @@ export function refine<T>(
   fn: (arg: NoInfer<T>) => util.MaybeAsync<unknown>,
   _params: string | core.$ZodCustomParams = {}
 ): core.$ZodCheck<T> {
-  return core._custom(ZodCustom, fn, _params);
+  return core._refine(ZodCustom, fn, _params);
 }
 
 // superRefine
diff --git a/packages/zod/src/v4/core/api.ts b/packages/zod/src/v4/core/api.ts
index 6f1da375..58851aee 100644
--- a/packages/zod/src/v4/core/api.ts
+++ b/packages/zod/src/v4/core/api.ts
@@ -1434,22 +1434,39 @@ export function _custom<O = unknown, I = O>(
   fn: (data: O) => unknown,
   _params: string | $ZodCustomParams | undefined
 ): schemas.$ZodCustom<O, I> {
+  const norm = util.normalizeParams(_params);
+  norm.abort ??= true; // default to abort:false
   const schema = new Class({
     type: "custom",
     check: "custom",
     fn: fn as any,
-    ...util.normalizeParams(_params),
+    ...norm,
   });
 
   return schema as any;
 }
 
-export function _refine<T>(
+// export function _refine<T>(
+//   Class: util.SchemaClass<schemas.$ZodCustom>,
+//   fn: (arg: NoInfer<T>) => util.MaybeAsync<unknown>,
+//   _params: string | $ZodCustomParams = {}
+// ): checks.$ZodCheck<T> {
+//   return _custom(Class, fn, _params);
+// }
+// same as _custom but deafults to abort:false
+export function _refine<O = unknown, I = O>(
   Class: util.SchemaClass<schemas.$ZodCustom>,
-  fn: (arg: NoInfer<T>) => util.MaybeAsync<unknown>,
-  _params: string | $ZodCustomParams = {}
-): checks.$ZodCheck<T> {
-  return _custom(Class, fn, _params);
+  fn: (data: O) => unknown,
+  _params: string | $ZodCustomParams | undefined
+): schemas.$ZodCustom<O, I> {
+  const schema = new Class({
+    type: "custom",
+    check: "custom",
+    fn: fn as any,
+    ...util.normalizeParams(_params),
+  });
+
+  return schema as any;
 }
 
 // export type $ZodCustomParams = CheckTypeParams<schemas.$ZodCustom, "fn">
diff --git a/packages/zod/src/v4/core/checks.ts b/packages/zod/src/v4/core/checks.ts
index 867bdfad..c0708c0f 100644
--- a/packages/zod/src/v4/core/checks.ts
+++ b/packages/zod/src/v4/core/checks.ts
@@ -622,6 +622,7 @@ export const $ZodCheckMaxLength: core.$constructor<$ZodCheckMaxLength> = /*@__PU
         origin,
         code: "too_big",
         maximum: def.maximum,
+        inclusive: true,
         input,
         inst,
         continue: !def.abort,
@@ -672,6 +673,7 @@ export const $ZodCheckMinLength: core.$constructor<$ZodCheckMinLength> = /*@__PU
         origin,
         code: "too_small",
         minimum: def.minimum,
+        inclusive: true,
         input,
         inst,
         continue: !def.abort,
@@ -952,7 +954,8 @@ export const $ZodCheckIncludes: core.$constructor<$ZodCheckIncludes> = /*@__PURE
   (inst, def) => {
     $ZodCheck.init(inst, def);
 
-    const pattern = new RegExp(util.escapeRegex(def.includes));
+    const escapedRegex = util.escapeRegex(def.includes);
+    const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
     def.pattern = pattern;
     inst._zod.onattach.push((inst) => {
       const bag = inst._zod.bag as schemas.$ZodStringInternals<unknown>["bag"];
diff --git a/packages/zod/src/v4/core/doc.ts b/packages/zod/src/v4/core/doc.ts
index 9cb6a508..9591fe7a 100644
--- a/packages/zod/src/v4/core/doc.ts
+++ b/packages/zod/src/v4/core/doc.ts
@@ -39,7 +39,6 @@ export class Doc {
     const content = this?.content ?? [``];
     const lines = [...content.map((x) => `  ${x}`)];
     // console.log(lines.join("\n"));
-    // console.dir("COMPILE", {depth: null});
     return new F(...args, lines.join("\n"));
   }
 }
diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts
index 29e4c57e..5425d12f 100644
--- a/packages/zod/src/v4/core/schemas.ts
+++ b/packages/zod/src/v4/core/schemas.ts
@@ -151,9 +151,11 @@ export interface $ZodTypeInternals<out O = unknown, out I = unknown> {
   parent?: $ZodType | undefined;
 }
 
+export type $ZodStandardSchema<T extends $ZodType> = StandardSchemaV1.Props<core.input<T>, core.output<T>>;
+
 export interface $ZodType<O = unknown, I = unknown> {
   _zod: $ZodTypeInternals<O, I>;
-  "~standard": StandardSchemaV1.Props<core.input<this>, core.output<this>>;
+  "~standard": $ZodStandardSchema<this>;
 }
 
 export const $ZodType: core.$constructor<$ZodType> = /*@__PURE__*/ core.$constructor("$ZodType", (inst, def) => {
@@ -1974,7 +1976,6 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
       const propValues: util.PropValues = {};
       for (const option of def.options) {
         const pv = option._zod.propValues;
-        console.dir(pv, { depth: null });
         if (!pv || Object.keys(pv).length === 0)
           throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
         for (const [k, v] of Object.entries(pv!)) {
@@ -2856,7 +2857,6 @@ export const $ZodTransform: core.$constructor<$ZodTransform> = /*@__PURE__*/ cor
     $ZodType.init(inst, def);
     inst._zod.parse = (payload, _ctx) => {
       const _out = def.transform(payload.value, payload);
-
       if (_ctx.async) {
         const output = _out instanceof Promise ? _out : Promise.resolve(_out);
         return output.then((output) => {
@@ -3372,7 +3372,6 @@ function handlePipeResult(left: ParsePayload, def: $ZodPipeDef, ctx: ParseContex
   if (util.aborted(left)) {
     return left;
   }
-
   return def.out._zod.run({ value: left.value, issues: left.issues }, ctx);
 }
 
diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts
index bae260bf..d710d606 100644
--- a/packages/zod/src/v4/core/to-json-schema.ts
+++ b/packages/zod/src/v4/core/to-json-schema.ts
@@ -432,8 +432,13 @@ export class JSONSchemaGenerator {
             // do nothing (an undefined literal was stripped)
           } else if (vals.length === 1) {
             const val = vals[0];
+            json.type = val === null ? "null" : typeof val;
             json.const = val;
           } else {
+            if (vals.every((v) => typeof v === "number")) json.type = "number";
+            if (vals.every((v) => typeof v === "string")) json.type = "string";
+            if (vals.every((v) => typeof v === "boolean")) json.type = "string";
+            if (vals.every((v) => v === null)) json.type = "null";
             json.enum = vals;
           }
           break;
diff --git a/packages/zod/src/v4/mini/schemas.ts b/packages/zod/src/v4/mini/schemas.ts
index 12bd5529..f2739923 100644
--- a/packages/zod/src/v4/mini/schemas.ts
+++ b/packages/zod/src/v4/mini/schemas.ts
@@ -1450,20 +1450,12 @@ export function check<O = unknown>(fn: core.CheckFn<O>, params?: string | core.$
 }
 
 // ZodCustom
-function _custom<O = unknown, I = O>(
-  fn: (data: O) => unknown,
-  _params: string | core.$ZodCustomParams | undefined,
-  Class: util.Constructor<ZodMiniCustom, [core.$ZodCustomDef]>
+// custom schema
+export function custom<O = unknown, I = O>(
+  fn?: (data: O) => unknown,
+  _params?: string | core.$ZodCustomParams | undefined
 ): ZodMiniCustom<O, I> {
-  const params = util.normalizeParams(_params);
-  const schema = new Class({
-    type: "custom",
-    check: "custom",
-    fn: fn as any,
-    ...params,
-  });
-
-  return schema as any;
+  return core._custom(ZodMiniCustom, fn ?? (() => true), _params) as any;
 }
 
 // refine
@@ -1471,15 +1463,7 @@ export function refine<T>(
   fn: (arg: NoInfer<T>) => util.MaybeAsync<unknown>,
   _params: string | core.$ZodCustomParams = {}
 ): core.$ZodCheck<T> {
-  return _custom(fn, _params, ZodMiniCustom);
-}
-
-// custom schema
-export function custom<O = unknown, I = O>(
-  fn?: (data: O) => unknown,
-  _params?: string | core.$ZodCustomParams | undefined
-): ZodMiniCustom<O, I> {
-  return _custom(fn ?? (() => true), _params, ZodMiniCustom);
+  return core._refine(ZodMiniCustom, fn, _params);
 }
 
 // instanceof
diff --git a/play.ts b/play.ts
index 33b0f819..58f4d9da 100644
--- a/play.ts
+++ b/play.ts
@@ -1,5 +1,38 @@
 import { z } from "zod/v4";
 
-z;
-const recordWithBrandedNumberKeys = z.record(z.number().brand("SomeBrand"), z.number());
-type recordWithBrandedNumberKeys = z.infer<typeof recordWithBrandedNumberKeys>;
+// const foo = z.preprocess((val, ctx) => {
+//   if (!val) {
+//     ctx.addIssue({ code: z.ZodIssueCode.custom, message: "bad" });
+//     return z.NEVER;
+//   }
+//   return val;
+// }, z.number());
+
+// type foo = z.infer<typeof foo>;
+// // util.assertEqual<foo, number>(true);
+// const arg = foo.safeParse(undefined);
+// console.dir(arg.error, { depth: null });
+
+// const schema2 = z
+//   .custom<number>((arg) => typeof arg === "number")
+//   .transform((arg) => {
+//     console.log(arg);
+//     return arg + 1;
+//   });
+
+// console.dir(schema2.safeParse("hello").error!, { depth: null });
+// const A = z
+//   .string()
+//   .refine((val) => false)
+//   .min(4)
+//   .transform((val) => val.length)
+//   .pipe(z.number())
+//   .refine((val) => false);
+// console.dir(A.safeParse("asdfasdf").error!, { depth: null });
+
+const A = z
+  .custom<string>((val) => typeof val === "string")
+  .refine((val) => {
+    return val.length > 3;
+  });
+A.parse(123);


Test patch:
diff --git a/packages/zod/src/v3/tests/preprocess.test.ts b/packages/zod/src/v3/tests/preprocess.test.ts
index d3782f25..31512970 100644
--- a/packages/zod/src/v3/tests/preprocess.test.ts
+++ b/packages/zod/src/v3/tests/preprocess.test.ts
@@ -136,9 +136,8 @@ test("z.NEVER in preprocess", () => {
   type foo = z.infer<typeof foo>;
   util.assertEqual<foo, number>(true);
   const arg = foo.safeParse(undefined);
-  if (!arg.success) {
-    expect(arg.error.issues[0].message).toEqual("bad");
-  }
+  expect(arg.error!.issues).toHaveLength(2);
+  expect(arg.error!.issues[0].message).toEqual("bad");
 });
 test("preprocess as the second property of object", () => {
   const schema = z.object({
diff --git a/packages/zod/src/v4/classic/tests/array.test.ts b/packages/zod/src/v4/classic/tests/array.test.ts
index 42982dd0..f19c252c 100644
--- a/packages/zod/src/v4/classic/tests/array.test.ts
+++ b/packages/zod/src/v4/classic/tests/array.test.ts
@@ -14,7 +14,8 @@ test("array min/max", async () => {
     [
       {
         "code": "too_small",
-        "message": "Too small: expected array to have >2 items",
+        "inclusive": true,
+        "message": "Too small: expected array to have >=2 items",
         "minimum": 2,
         "origin": "array",
         "path": [],
@@ -28,8 +29,9 @@ test("array min/max", async () => {
     [
       {
         "code": "too_big",
+        "inclusive": true,
         "maximum": 2,
-        "message": "Too big: expected array to have <2 items",
+        "message": "Too big: expected array to have <=2 items",
         "origin": "array",
         "path": [],
       },
@@ -122,10 +124,11 @@ test("continue parsing despite array size error", () => {
         "origin": "array",
         "code": "too_small",
         "minimum": 2,
+        "inclusive": true,
         "path": [
           "people"
         ],
-        "message": "Too small: expected array to have >2 items"
+        "message": "Too small: expected array to have >=2 items"
       }
     ]],
       "success": false,
diff --git a/packages/zod/src/v4/classic/tests/custom.test.ts b/packages/zod/src/v4/classic/tests/custom.test.ts
index c360a216..b77f8115 100644
--- a/packages/zod/src/v4/classic/tests/custom.test.ts
+++ b/packages/zod/src/v4/classic/tests/custom.test.ts
@@ -21,3 +21,20 @@ test("instanceof", () => {
   // Argument of type 'ZodCustom<Uint8Array<ArrayBuffer>, unknown>' is not assignable to parameter of type '$ZodType<any, Uint8Array<ArrayBuffer>>'.
   z.string().transform(fn).pipe(z.instanceof(Uint8Array));
 });
+
+test("non-continuable by default", () => {
+  const A = z
+    .custom<string>((val) => typeof val === "string")
+    .transform((_) => {
+      throw new Error("Invalid input");
+    });
+  expect(A.safeParse(123).error!).toMatchInlineSnapshot(`
+    [ZodError: [
+      {
+        "code": "custom",
+        "path": [],
+        "message": "Invalid input"
+      }
+    ]]
+  `);
+});
diff --git a/packages/zod/src/v4/classic/tests/error.test.ts b/packages/zod/src/v4/classic/tests/error.test.ts
index 19e25abf..d884b7e4 100644
--- a/packages/zod/src/v4/classic/tests/error.test.ts
+++ b/packages/zod/src/v4/classic/tests/error.test.ts
@@ -176,8 +176,9 @@ test("array minimum", () => {
         "origin": "array",
         "code": "too_small",
         "minimum": 3,
+        "inclusive": true,
         "path": [],
-        "message": "Too small: expected array to have >3 items"
+        "message": "Too small: expected array to have >=3 items"
       }
     ]]
   `);
@@ -530,6 +531,7 @@ test("z.config customError ", () => {
         "origin": "string",
         "code": "too_small",
         "minimum": 10,
+        "inclusive": true,
         "path": [],
         "message": "override"
       }
@@ -614,10 +616,11 @@ test("dont short circuit on continuable errors", () => {
         "origin": "string",
         "code": "too_small",
         "minimum": 6,
+        "inclusive": true,
         "path": [
           "password"
         ],
-        "message": "Too small: expected string to have >6 characters"
+        "message": "Too small: expected string to have >=6 characters"
       },
       {
         "code": "custom",
diff --git a/packages/zod/src/v4/classic/tests/nested-refine.test.ts b/packages/zod/src/v4/classic/tests/nested-refine.test.ts
index 0d3401e9..b33b9ef4 100644
--- a/packages/zod/src/v4/classic/tests/nested-refine.test.ts
+++ b/packages/zod/src/v4/classic/tests/nested-refine.test.ts
@@ -45,11 +45,12 @@ test("nested refinements", () => {
         "origin": "string",
         "code": "too_small",
         "minimum": 1,
+        "inclusive": true,
         "path": [
           "nested",
           "confirm"
         ],
-        "message": "Too small: expected string to have >1 characters"
+        "message": "Too small: expected string to have >=1 characters"
       },
       {
         "code": "custom",
@@ -87,11 +88,12 @@ test("nested refinements", () => {
         "origin": "string",
         "code": "too_small",
         "minimum": 1,
+        "inclusive": true,
         "path": [
           "nested",
           "confirm"
         ],
-        "message": "Too small: expected string to have >1 characters"
+        "message": "Too small: expected string to have >=1 characters"
       },
       {
         "code": "custom",
@@ -127,7 +129,8 @@ test("nested refinements", () => {
       "issues": [
         {
           "code": "too_small",
-          "message": "Too small: expected string to have >1 characters",
+          "inclusive": true,
+          "message": "Too small: expected string to have >=1 characters",
           "minimum": 1,
           "origin": "string",
           "path": [
diff --git a/packages/zod/src/v4/classic/tests/preprocess.test.ts b/packages/zod/src/v4/classic/tests/preprocess.test.ts
index 6a8faee4..d782df06 100644
--- a/packages/zod/src/v4/classic/tests/preprocess.test.ts
+++ b/packages/zod/src/v4/classic/tests/preprocess.test.ts
@@ -214,10 +214,11 @@ test("preprocess as the second property of object", () => {
         "origin": "string",
         "code": "too_small",
         "minimum": 1,
+        "inclusive": true,
         "path": [
           "nonEmptyStr"
         ],
-        "message": "Too small: expected string to have >1 characters"
+        "message": "Too small: expected string to have >=1 characters"
       },
       {
         "origin": "number",
@@ -270,3 +271,28 @@ test("preprocess validates with sibling errors", () => {
     }
   `);
 });
+
+test("perform transform with non-fatal issues", () => {
+  const A = z
+    .string()
+    .refine((_) => false)
+    .min(4)
+    .transform((val) => val.length)
+    .pipe(z.number())
+    .refine((_) => false);
+  expect(A.safeParse("asdfasdf").error!.issues).toHaveLength(2);
+  expect(A.safeParse("asdfasdf").error).toMatchInlineSnapshot(`
+    [ZodError: [
+      {
+        "code": "custom",
+        "path": [],
+        "message": "Invalid input"
+      },
+      {
+        "code": "custom",
+        "path": [],
+        "message": "Invalid input"
+      }
+    ]]
+  `);
+});
diff --git a/packages/zod/src/v4/classic/tests/to-json-schema.test.ts b/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
index d8684c94..34671072 100644
--- a/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
+++ b/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
@@ -365,7 +365,15 @@ describe("toJSONSchema", () => {
 
   test("string patterns", () => {
     expect(
-      z.toJSONSchema(z.string().startsWith("hello").includes("cruel").endsWith("world").regex(/stuff/))
+      z.toJSONSchema(
+        z
+          .string()
+          .startsWith("hello")
+          .includes("cruel")
+          .includes("dark", { position: 10 })
+          .endsWith("world")
+          .regex(/stuff/)
+      )
     ).toMatchInlineSnapshot(`
       {
         "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -376,6 +384,9 @@ describe("toJSONSchema", () => {
           {
             "pattern": "cruel",
           },
+          {
+            "pattern": "^.{10}dark",
+          },
           {
             "pattern": ".*world$",
           },
@@ -388,9 +399,18 @@ describe("toJSONSchema", () => {
     `);
 
     expect(
-      z.toJSONSchema(z.string().startsWith("hello").includes("cruel").endsWith("world").regex(/stuff/), {
-        target: "draft-7",
-      })
+      z.toJSONSchema(
+        z
+          .string()
+          .startsWith("hello")
+          .includes("cruel")
+          .includes("dark", { position: 10 })
+          .endsWith("world")
+          .regex(/stuff/),
+        {
+          target: "draft-7",
+        }
+      )
     ).toMatchInlineSnapshot(`
       {
         "$schema": "http://json-schema.org/draft-07/schema#",
@@ -403,6 +423,10 @@ describe("toJSONSchema", () => {
             "pattern": "cruel",
             "type": "string",
           },
+          {
+            "pattern": "^.{10}dark",
+            "type": "string",
+          },
           {
             "pattern": ".*world$",
             "type": "string",
@@ -679,14 +703,24 @@ describe("toJSONSchema", () => {
       {
         "$schema": "https://json-schema.org/draft/2020-12/schema",
         "const": "hello",
+        "type": "string",
       }
     `);
 
-    const b = z.literal(["hello", undefined, null, 5, BigInt(1324)]);
-    expect(() => z.toJSONSchema(b)).toThrow();
+    const b = z.literal(7);
+    expect(z.toJSONSchema(b)).toMatchInlineSnapshot(`
+      {
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
+        "const": 7,
+        "type": "number",
+      }
+    `);
 
-    const c = z.literal(["hello", null, 5]);
-    expect(z.toJSONSchema(c)).toMatchInlineSnapshot(`
+    const c = z.literal(["hello", undefined, null, 5, BigInt(1324)]);
+    expect(() => z.toJSONSchema(c)).toThrow();
+
+    const d = z.literal(["hello", null, 5]);
+    expect(z.toJSONSchema(d)).toMatchInlineSnapshot(`
       {
         "$schema": "https://json-schema.org/draft/2020-12/schema",
         "enum": [
@@ -696,6 +730,19 @@ describe("toJSONSchema", () => {
         ],
       }
     `);
+
+    const e = z.literal(["hello", "zod", "v4"]);
+    expect(z.toJSONSchema(e)).toMatchInlineSnapshot(`
+      {
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
+        "enum": [
+          "hello",
+          "zod",
+          "v4",
+        ],
+        "type": "string",
+      }
+    `);
   });
 
   // pipe
diff --git a/packages/zod/src/v4/classic/tests/validations.test.ts b/packages/zod/src/v4/classic/tests/validations.test.ts
index 6be5d40f..fecc2893 100644
--- a/packages/zod/src/v4/classic/tests/validations.test.ts
+++ b/packages/zod/src/v4/classic/tests/validations.test.ts
@@ -47,7 +47,8 @@ test("string min/max", async () => {
       [
         {
           "code": "too_small",
-          "message": "Too small: expected string to have >4 characters",
+          "inclusive": true,
+          "message": "Too small: expected string to have >=4 characters",
           "minimum": 4,
           "origin": "string",
           "path": [],
@@ -66,8 +67,9 @@ test("string max", async () => {
       [
         {
           "code": "too_big",
+          "inclusive": true,
           "maximum": 4,
-          "message": "Too big: expected string to have <4 characters",
+          "message": "Too big: expected string to have <=4 characters",
           "origin": "string",
           "path": [],
         },
diff --git a/vitest.root.mts b/vitest.root.mts
index d9cf889a..f9e349f2 100644
--- a/vitest.root.mts
+++ b/vitest.root.mts
@@ -1,4 +1,4 @@
-import { type UserConfig, defineConfig } from "vitest/config";
+import { type ViteUserConfig, defineConfig } from "vitest/config";
 
 export default defineConfig({
   resolve: {
@@ -14,7 +14,6 @@ export default defineConfig({
       checker: "tsc",
       tsconfig: "./tsconfig.json",
     },
-
     silent: true,
   },
-}) as UserConfig;
+}) as ViteUserConfig;