Skip to content
Closed
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
15 changes: 15 additions & 0 deletions packages/zod/src/v4/classic/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ export const parseAsync: <T extends core.$ZodType>(
_params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass }
) => Promise<core.output<T>> = /* @__PURE__ */ core._parseAsync(ZodRealError);

export const parseMaybeAsync: <T extends core.$ZodType>(
schema: T,
value: unknown,
_ctx?: core.ParseContext<core.$ZodIssue>,
_params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass }
) => core.util.MaybeAsync<core.output<T>> = /* @__PURE__ */ core._parseMaybeAsync(ZodRealError);

export const safeParse: <T extends core.$ZodType>(
schema: T,
value: unknown,
Expand All @@ -32,6 +39,14 @@ export const safeParseAsync: <T extends core.$ZodType>(
_ctx?: core.ParseContext<core.$ZodIssue>
) => Promise<ZodSafeParseResult<core.output<T>>> = /* @__PURE__ */ core._safeParseAsync(ZodRealError) as any;

export const safeParseMaybeAsync: <T extends core.$ZodType>(
schema: T,
value: unknown,
_ctx?: core.ParseContext<core.$ZodIssue>
) => core.util.MaybeAsync<ZodSafeParseResult<core.output<T>>> = /* @__PURE__ */ core._safeParseMaybeAsync(
ZodRealError
) as any;

// Codec functions
export const encode: <T extends core.$ZodType>(
schema: T,
Expand Down
7 changes: 7 additions & 0 deletions packages/zod/src/v4/classic/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export interface ZodType<
parse(data: unknown, params?: core.ParseContext<core.$ZodIssue>): core.output<this>;
safeParse(data: unknown, params?: core.ParseContext<core.$ZodIssue>): parse.ZodSafeParseResult<core.output<this>>;
parseAsync(data: unknown, params?: core.ParseContext<core.$ZodIssue>): Promise<core.output<this>>;
parseMaybeAsync(data: unknown, params?: core.ParseContext<core.$ZodIssue>): core.util.MaybeAsync<core.output<this>>;
safeParseMaybeAsync(
data: unknown,
params?: core.ParseContext<core.$ZodIssue>
): core.util.MaybeAsync<parse.ZodSafeParseResult<core.output<this>>>;
safeParseAsync(
data: unknown,
params?: core.ParseContext<core.$ZodIssue>
Expand Down Expand Up @@ -236,6 +241,8 @@ export const ZodType: core.$constructor<ZodType> = /*@__PURE__*/ core.$construct
inst.safeParse = (data, params) => parse.safeParse(inst, data, params);
inst.parseAsync = async (data, params) => parse.parseAsync(inst, data, params, { callee: inst.parseAsync });
inst.safeParseAsync = async (data, params) => parse.safeParseAsync(inst, data, params);
inst.parseMaybeAsync = (data, params) => parse.parseMaybeAsync(inst, data, params, { callee: inst.parseMaybeAsync });
inst.safeParseMaybeAsync = (data, params) => parse.safeParseMaybeAsync(inst, data, params);
inst.spa = inst.safeParseAsync;
inst.encode = (data, params) => parse.encode(inst, data, params);
inst.decode = (data, params) => parse.decode(inst, data, params);
Expand Down
72 changes: 72 additions & 0 deletions packages/zod/src/v4/classic/tests/parse-maybe-async.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect, test } from "vitest";
import * as z from "zod/v4";

// Tests for #5379.

test("parseMaybeAsync returns the value sync when no async work is involved", () => {
const schema = z.string();
const result = schema.parseMaybeAsync("hello");
expect(result instanceof Promise).toBe(false);
expect(result).toBe("hello");
});

test("parseMaybeAsync returns a Promise when an async refinement is encountered", async () => {
const schema = z.string().refine(async (v) => v.length > 0);
const result = schema.parseMaybeAsync("hello");
expect(result instanceof Promise).toBe(true);
await expect(result).resolves.toBe("hello");
});

test("parseMaybeAsync throws sync on validation failure with no async work", () => {
const schema = z.string();
expect(() => schema.parseMaybeAsync(42)).toThrow(z.ZodError);
});

test("parseMaybeAsync rejects with ZodError on async validation failure", async () => {
const schema = z.string().refine(async (v) => v.length > 5, { message: "too short" });
const result = schema.parseMaybeAsync("hi");
expect(result instanceof Promise).toBe(true);
await expect(result).rejects.toThrow(z.ZodError);
});

test("safeParseMaybeAsync mirrors parseMaybeAsync but wraps the result", () => {
const sync = z.string().safeParseMaybeAsync("hi");
expect(sync instanceof Promise).toBe(false);
expect(sync).toEqual({ success: true, data: "hi" });

const failure = z.string().safeParseMaybeAsync(42);
expect(failure instanceof Promise).toBe(false);
if (!(failure instanceof Promise)) expect(failure.success).toBe(false);
});

test("safeParseMaybeAsync awaits async refinements (success)", async () => {
const schema = z.string().refine(async (v) => v.length > 0);
const result = schema.safeParseMaybeAsync("hi");
expect(result instanceof Promise).toBe(true);
const settled = await result;
expect(settled).toEqual({ success: true, data: "hi" });
});

test("safeParseMaybeAsync awaits async refinements (failure)", async () => {
const schema = z.string().refine(async (v) => v.length > 5, { message: "too short" });
const result = schema.safeParseMaybeAsync("hi");
expect(result instanceof Promise).toBe(true);
const settled = await result;
expect(settled.success).toBe(false);
if (!settled.success) expect(settled.error.issues[0].message).toBe("too short");
});

// Standard Schema integration: validate() now uses safeParseMaybeAsync
// internally, so a fully-sync schema must NOT return a Promise.
test("StandardSchema validate is sync when schema has no async work", () => {
const schema = z.string();
const validated = schema["~standard"].validate("hi");
expect(validated instanceof Promise).toBe(false);
});

test("StandardSchema validate is async when schema has async refinements", async () => {
const schema = z.string().refine(async (v) => v.length > 0);
const validated = schema["~standard"].validate("hi");
expect(validated instanceof Promise).toBe(true);
await expect(validated).resolves.toEqual({ value: "hi" });
});
42 changes: 42 additions & 0 deletions packages/zod/src/v4/core/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@ export const _parseAsync: (_Err: $ZodErrorClass) => $ParseAsync = (_Err) => asyn

export const parseAsync: $ParseAsync = /* @__PURE__*/ _parseAsync(errors.$ZodRealError);

export type $ParseMaybeAsync = <T extends schemas.$ZodType>(
schema: T,
value: unknown,
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
_params?: { callee?: util.AnyFunc; Err?: $ZodErrorClass }
) => util.MaybeAsync<core.output<T>>;

export const _parseMaybeAsync: (_Err: $ZodErrorClass) => $ParseMaybeAsync =
(_Err) => (schema, value, _ctx, _params) => {
const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: true } : { async: true };
const finalize = (r: schemas.ParsePayload) => {
if (r.issues.length) {
const e = new (_params?.Err ?? _Err)(r.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));
util.captureStackTrace(e, _params?.callee);
throw e;
}
return r.value as core.output<typeof schema>;
};
const result = schema._zod.run({ value, issues: [] }, ctx);
return result instanceof Promise ? result.then(finalize) : finalize(result);
};

export const parseMaybeAsync: $ParseMaybeAsync = /* @__PURE__*/ _parseMaybeAsync(errors.$ZodRealError);

export type $SafeParse = <T extends schemas.$ZodType>(
schema: T,
value: unknown,
Expand Down Expand Up @@ -93,6 +117,24 @@ export const _safeParseAsync: (_Err: $ZodErrorClass) => $SafeParseAsync = (_Err)

export const safeParseAsync: $SafeParseAsync = /* @__PURE__*/ _safeParseAsync(errors.$ZodRealError);

export type $SafeParseMaybeAsync = <T extends schemas.$ZodType>(
schema: T,
value: unknown,
_ctx?: schemas.ParseContext<errors.$ZodIssue>
) => util.MaybeAsync<util.SafeParseResult<core.output<T>>>;

export const _safeParseMaybeAsync: (_Err: $ZodErrorClass) => $SafeParseMaybeAsync = (_Err) => (schema, value, _ctx) => {
const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: true } : { async: true };
const finalize = (r: schemas.ParsePayload) =>
r.issues.length
? { success: false, error: new _Err(r.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))) }
: { success: true, data: r.value };
const result = schema._zod.run({ value, issues: [] }, ctx);
return (result instanceof Promise ? result.then(finalize) : finalize(result)) as any;
};

export const safeParseMaybeAsync: $SafeParseMaybeAsync = /* @__PURE__*/ _safeParseMaybeAsync(errors.$ZodRealError);

// Codec functions
export type $Encode = <T extends schemas.$ZodType>(
schema: T,
Expand Down
11 changes: 5 additions & 6 deletions packages/zod/src/v4/core/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as core from "./core.js";
import { Doc } from "./doc.js";
import type * as errors from "./errors.js";
import type * as JSONSchema from "./json-schema.js";
import { parse, parseAsync, safeParse, safeParseAsync } from "./parse.js";
import { parse, parseAsync, safeParseMaybeAsync } from "./parse.js";
import * as regexes from "./regexes.js";
import type { StandardSchemaV1 } from "./standard-schema.js";
import type { ProcessParams, ToJSONSchemaContext } from "./to-json-schema.js";
Expand Down Expand Up @@ -302,12 +302,11 @@ export const $ZodType: core.$constructor<$ZodType> = /*@__PURE__*/ core.$constru
// Lazy initialize ~standard to avoid creating objects for every schema
util.defineLazy(inst, "~standard", () => ({
validate: (value: unknown) => {
try {
const r = safeParse(inst, value);
return r.success ? { value: r.data } : { issues: r.error?.issues };
} catch (_) {
return safeParseAsync(inst, value).then((r) => (r.success ? { value: r.data } : { issues: r.error?.issues }));
const r = safeParseMaybeAsync(inst, value);
if (r instanceof Promise) {
return r.then((rr) => (rr.success ? { value: rr.data } : { issues: rr.error?.issues }));
}
return r.success ? { value: r.data } : { issues: r.error?.issues };
},
vendor: "zod",
version: 1 as const,
Expand Down
2 changes: 2 additions & 0 deletions packages/zod/src/v4/mini/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export {
safeParse,
parseAsync,
safeParseAsync,
parseMaybeAsync,
safeParseMaybeAsync,
encode,
decode,
encodeAsync,
Expand Down
8 changes: 8 additions & 0 deletions packages/zod/src/v4/mini/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export interface ZodMiniType<
data: unknown,
params?: core.ParseContext<core.$ZodIssue>
): Promise<util.SafeParseResult<core.output<this>>>;
parseMaybeAsync(data: unknown, params?: core.ParseContext<core.$ZodIssue>): util.MaybeAsync<core.output<this>>;
safeParseMaybeAsync(
data: unknown,
params?: core.ParseContext<core.$ZodIssue>
): util.MaybeAsync<util.SafeParseResult<core.output<this>>>;
apply<T>(fn: (schema: this) => T): T;
}

Expand All @@ -53,6 +58,9 @@ export const ZodMiniType: core.$constructor<ZodMiniType> = /*@__PURE__*/ core.$c
inst.safeParse = (data, params) => parse.safeParse(inst, data, params);
inst.parseAsync = async (data, params) => parse.parseAsync(inst, data, params, { callee: inst.parseAsync });
inst.safeParseAsync = async (data, params) => parse.safeParseAsync(inst, data, params);
inst.parseMaybeAsync = (data, params) =>
parse.parseMaybeAsync(inst, data, params, { callee: inst.parseMaybeAsync });
inst.safeParseMaybeAsync = (data, params) => parse.safeParseMaybeAsync(inst, data, params);
inst.check = (...checks) => {
return inst.clone(
{
Expand Down
27 changes: 27 additions & 0 deletions packages/zod/src/v4/mini/tests/parse-maybe-async.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from "vitest";
import * as z from "zod/v4-mini";

// Mini-side coverage for #5379 — methods are wired the same way on ZodMiniType.

test("ZodMiniType.parseMaybeAsync stays sync when nothing is async", () => {
const result = z.string().parseMaybeAsync("hi");
expect(result instanceof Promise).toBe(false);
expect(result).toBe("hi");
});

test("ZodMiniType.parseMaybeAsync returns a Promise on async refinement", async () => {
const schema = z.string().check(z.refine(async (v) => v.length > 0));
const result = schema.parseMaybeAsync("hi");
expect(result instanceof Promise).toBe(true);
await expect(result).resolves.toBe("hi");
});

test("ZodMiniType.safeParseMaybeAsync sync success / failure", () => {
const success = z.string().safeParseMaybeAsync("hi");
expect(success instanceof Promise).toBe(false);
expect(success).toEqual({ success: true, data: "hi" });

const failure = z.string().safeParseMaybeAsync(42);
expect(failure instanceof Promise).toBe(false);
if (!(failure instanceof Promise)) expect(failure.success).toBe(false);
});
Loading