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
5 changes: 5 additions & 0 deletions .changeset/zod-v4-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

fix: add zod/v4 fallback for toJSONSchema detection (fixes #1845)
41 changes: 38 additions & 3 deletions packages/core/lib/v3/zodCompat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import type {
} from "zod";
import zodToJsonSchema from "zod-to-json-schema";
import type * as z3 from "zod/v3";
import { createRequire } from "node:module";
import { getCurrentFilePath } from "./runtimePaths.js";
import { StagehandError } from "./types/public/sdkErrors.js";
export type StagehandZodSchema = Zod4TypeAny | z3.ZodTypeAny;

export type StagehandZodObject =
Expand All @@ -30,12 +33,35 @@ export const isZod3Schema = (

export type JsonSchemaDocument = Record<string, unknown>;

// Lazy-init fallback: in transitional zod versions (e.g. 3.25.x), the root
// "zod" import is a v3-compat layer without toJSONSchema, but the real v4 API
// is available at "zod/v4". We resolve it once on first use.
let _zodV4ToJSONSchema: ((schema: Zod4TypeAny) => JsonSchemaDocument) | null =
null;
let _zodV4Resolved = false;

function getZodV4ToJSONSchema(): typeof _zodV4ToJSONSchema {
if (!_zodV4Resolved) {
_zodV4Resolved = true;
try {
const req = createRequire(getCurrentFilePath());
Copy link
Copy Markdown
Member

@pirate pirate Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure this works with both our scripts/build-esm.ts and scripts/build-cjs.ts distributions?

iirc dynamic imports dont work nicely across both, ideally I'd rather keep a static import at the top for both but try/except wrap it and use the one that succeeds?

const zodV4 = req("zod/v4") as {
toJSONSchema?: (schema: Zod4TypeAny) => JsonSchemaDocument;
};
_zodV4ToJSONSchema = zodV4.toJSONSchema ?? null;
} catch {
// zod/v4 subpath not available — will fall through to error below
}
}
return _zodV4ToJSONSchema;
}

export function toJsonSchema(schema: StagehandZodSchema): JsonSchemaDocument {
if (!isZod4Schema(schema)) {
return zodToJsonSchema(schema);
}

// For v4 schemas, use built-in z.toJSONSchema() method
// For v4 schemas, try the root z.toJSONSchema() first (works with zod >= 4.x)
const zodWithJsonSchema = z as typeof z & {
toJSONSchema?: (schema: Zod4TypeAny) => JsonSchemaDocument;
};
Expand All @@ -44,6 +70,15 @@ export function toJsonSchema(schema: StagehandZodSchema): JsonSchemaDocument {
return zodWithJsonSchema.toJSONSchema(schema as Zod4TypeAny);
}

// This should never happen with Zod v4.1+
throw new Error("Zod v4 toJSONSchema method not found");
// Fallback: in transitional zod 3.25.x the root "zod" is v3, but
// "zod/v4" exposes toJSONSchema.
const v4Fallback = getZodV4ToJSONSchema();
if (v4Fallback) {
return v4Fallback(schema as Zod4TypeAny);
}

throw new StagehandError(
"Zod v4 schema detected but toJSONSchema is unavailable. " +
'Ensure your zod version exposes toJSONSchema on the root export or via "zod/v4".',
);
}
Loading