Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/zod/src/v4/classic/tests/error-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ test(".flatten()", () => {
});

test("custom .flatten()", () => {
type ErrorType = { message: string; code: number };
type ErrorType = { message: string | { key: string; values?: object }; code: number };
const flattened = parsed.error!.flatten((iss) => ({ message: iss.message, code: 1234 }));
expectTypeOf(flattened).toMatchTypeOf<{
formErrors: ErrorType[];
Expand Down Expand Up @@ -159,7 +159,7 @@ test(".format()", () => {
});

test("custom .format()", () => {
type ErrorType = { message: string; code: number };
type ErrorType = { message: string | { key: string; values?: object }; code: number };
const formatted = parsed.error!.format((iss) => ({ message: iss.message, code: 1234 }));
expectTypeOf(formatted).toMatchTypeOf<{
_errors: ErrorType[];
Expand Down Expand Up @@ -252,7 +252,7 @@ test("all errors", () => {
}
`);

expect(z.core.flattenError(r2.error!, (iss) => iss.message.toUpperCase())).toMatchInlineSnapshot(`
expect(z.core.flattenError(r2.error!, (iss) => (iss.message as string).toUpperCase())).toMatchInlineSnapshot(`
{
"fieldErrors": {
"a": [
Expand Down Expand Up @@ -296,7 +296,7 @@ test("all errors", () => {
`);

// Test mapping
const f1 = z.core.flattenError(r2.error!, (i: z.ZodIssue) => i.message.length);
const f1 = z.core.flattenError(r2.error!, (i: z.ZodIssue) => (i.message as string).length);
expect(f1).toMatchInlineSnapshot(`
{
"fieldErrors": {
Expand Down
27 changes: 27 additions & 0 deletions packages/zod/src/v4/classic/tests/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,30 @@ test("error serialization", () => {
`);
}
});

test("error with object type message", () => {
const schema = z.object({
name: z.string().min(5, { error: () => ({ message: { key: "test", values: { foo: "bar" } } }) }),
});

const obj = {
name: "tes",
};

const result = schema.safeParse(obj);
expect(result.success).toBe(false);
expect(result.error).toMatchInlineSnapshot(`
[ZodError: [
{
"origin": "string",
"code": "too_small",
"minimum": 5,
"inclusive": true,
"path": [
"name"
],
"message": "{\\"key\\":\\"test\\",\\"values\\":{\\"foo\\":\\"bar\\"}}"
}
]]
`);
});
16 changes: 10 additions & 6 deletions packages/zod/src/v4/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { $constructor } from "./core.js";
import type { $ZodType } from "./schemas.js";
import type { StandardSchemaV1 } from "./standard-schema.js";
import * as util from "./util.js";
import { toMessageString } from "./util.js";

export type $ZodIssueMessage = string | { key: string; values?: object };

///////////////////////////
//// base type ////
Expand All @@ -11,7 +14,7 @@ export interface $ZodIssueBase {
readonly code?: string;
readonly input?: unknown;
readonly path: PropertyKey[];
readonly message: string;
readonly message: $ZodIssueMessage;
}

////////////////////////////////
Expand Down Expand Up @@ -174,7 +177,7 @@ export type $ZodRawIssue<T extends $ZodIssueBase = $ZodIssue> = $ZodInternalIssu

export interface $ZodErrorMap<T extends $ZodIssueBase = $ZodIssue> {
// biome-ignore lint:
(issue: $ZodRawIssue<T>): { message: string } | string | undefined | null;
(issue: $ZodRawIssue<T>): { message: $ZodIssueMessage } | string | undefined | null;
}

//////////////////////// ERROR CLASS ////////////////////////
Expand Down Expand Up @@ -226,7 +229,7 @@ type _FlattenedError<T, U = string> = {

export function flattenError<T>(error: $ZodError<T>): _FlattenedError<T>;
export function flattenError<T, U>(error: $ZodError<T>, mapper?: (issue: $ZodIssue) => U): _FlattenedError<T, U>;
export function flattenError(error: $ZodError, mapper = (issue: $ZodIssue) => issue.message): any {
export function flattenError(error: $ZodError, mapper = (issue: $ZodIssue) => toMessageString(issue.message)): any {
const fieldErrors: any = {};
const formErrors: any[] = [];
for (const sub of error.issues) {
Expand Down Expand Up @@ -258,7 +261,7 @@ export function formatError<T>(error: $ZodError, _mapper?: any) {
const mapper: (issue: $ZodIssue) => any =
_mapper ||
function (issue: $ZodIssue) {
return issue.message;
return toMessageString(issue.message);
};
const fieldErrors: $ZodFormattedError<T> = { _errors: [] } as any;
const processError = (error: { issues: $ZodIssue[] }) => {
Expand Down Expand Up @@ -311,7 +314,7 @@ export function treeifyError<T>(error: $ZodError, _mapper?: any) {
const mapper: (issue: $ZodIssue) => any =
_mapper ||
function (issue: $ZodIssue) {
return issue.message;
return toMessageString(issue.message);
};
const result: $ZodErrorTree<T> = { errors: [] } as any;
const processError = (error: { issues: $ZodIssue[] }, path: PropertyKey[] = []) => {
Expand Down Expand Up @@ -414,7 +417,8 @@ export function prettifyError(error: StandardSchemaV1.FailureResult): string {

// Process each issue
for (const issue of issues) {
lines.push(`✖ ${issue.message}`);
const m = toMessageString(issue.message);
lines.push(`✖ ${m}`);
if (issue.path?.length) lines.push(` → at ${toDotPath(issue.path)}`);
}

Expand Down
4 changes: 3 additions & 1 deletion packages/zod/src/v4/core/standard-schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { $ZodIssueMessage } from "./errors.js";

/** The Standard Schema interface. */
export interface StandardSchemaV1<Input = unknown, Output = Input> {
/** The Standard Schema properties. */
Expand Down Expand Up @@ -37,7 +39,7 @@ export declare namespace StandardSchemaV1 {
/** The issue interface of the failure output. */
export interface Issue {
/** The error message of the issue. */
readonly message: string;
readonly message: $ZodIssueMessage;
/** The path of the issue, if any. */
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
}
Expand Down
16 changes: 14 additions & 2 deletions packages/zod/src/v4/core/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type * as checks from "./checks.js";
import type { $ZodConfig } from "./core.js";
import type * as errors from "./errors.js";
import type { $ZodIssueMessage } from "./errors.js";
import type * as schemas from "./schemas.js";

// json
Expand Down Expand Up @@ -786,8 +787,11 @@ export function prefixIssues(path: PropertyKey, issues: errors.$ZodRawIssue[]):
});
}

export function unwrapMessage(message: string | { message: string } | undefined | null): string | undefined {
return typeof message === "string" ? message : message?.message;
export function unwrapMessage(message: string | { message: $ZodIssueMessage } | undefined | null): string | undefined {
if (message === undefined) {
return undefined;
}
return typeof message === "string" ? message : toMessageString(message?.message || "");
}

export function finalizeIssue(
Expand Down Expand Up @@ -908,3 +912,11 @@ export function uint8ArrayToHex(bytes: Uint8Array): string {
export abstract class Class {
constructor(..._args: any[]) {}
}

export function toMessageString(message: $ZodIssueMessage): string {
if (typeof message === "string") {
return message;
} else {
return JSON.stringify(message);
}
}