Skip to content

fix: add zod/v4 fallback for toJSONSchema detection (fixes #1845)#1918

Closed
kagura-agent wants to merge 3 commits intobrowserbase:mainfrom
kagura-agent:fix/zod-v4-schema-detection
Closed

fix: add zod/v4 fallback for toJSONSchema detection (fixes #1845)#1918
kagura-agent wants to merge 3 commits intobrowserbase:mainfrom
kagura-agent:fix/zod-v4-schema-detection

Conversation

@kagura-agent
Copy link
Copy Markdown

@kagura-agent kagura-agent commented Mar 30, 2026

Summary

Fixes #1845. When zod@3.25.76 (transitional version) is installed, the root zod import resolves to a v3 compatibility layer that lacks toJSONSchema. This causes a runtime crash when Stagehand detects a Zod v4 schema (via _zod property) but cannot find the conversion method.

Changes

packages/core/lib/v3/zodCompat.ts (+37/-3 lines)

  • Added getZodV4ToJSONSchema() lazy-loading function that attempts to import toJSONSchema from the zod/v4 subpath as a fallback
  • Uses createRequire(getCurrentFilePath()) for ESM/CJS compatibility (reuses existing runtimePaths.js helper)
  • Resolution order: root z.toJSONSchema() → fallback zod/v4 subpath → clear error message
  • Result is cached after first resolution to avoid repeated dynamic imports

Testing

  • 37 test files passed, 347 tests passed, 0 new failures
  • tsc --noEmit clean for modified file
  • 9 pre-existing test failures in upstream (public-api package resolution + flowlogger assertions)

Summary by cubic

Adds a fallback to load toJSONSchema from zod/v4 when the root zod export doesn’t expose it, preventing crashes on transitional installs. Includes a changeset to publish a patch for @browserbasehq/stagehand.

  • Bug Fixes

    • Resolution order: root z.toJSONSchema()zod/v4 → clear error if unavailable.
    • Lazy-loads and caches the v4 resolver; ESM/CJS safe via createRequire and getCurrentFilePath().
  • Refactors

    • Replace generic Error with StagehandError for consistent error handling.

Written for commit b8dc165. Summary will update on new commits. Review in cubic

…e#1845)

In transitional zod versions (e.g. 3.25.x), the root 'zod' import is a
v3-compat layer that doesn't expose toJSONSchema, even though schemas
created via zod/v4 have the _zod property. This caused toJsonSchema() to
throw 'Zod v4 toJSONSchema method not found' when a v4 schema was passed
but the root zod lacked the method.

Add a lazy-init fallback that dynamically loads 'zod/v4' via
createRequire and caches its toJSONSchema function. The resolution uses
the project's existing getCurrentFilePath() helper from runtimePaths.js,
which works in both ESM and CJS contexts.

Resolution order:
1. Root z.toJSONSchema() (works with zod >= 4.x)
2. Fallback to zod/v4 subpath toJSONSchema (transitional 3.25.x)
3. Clear error message if neither is available
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 30, 2026

🦋 Changeset detected

Latest commit: b8dc165

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@browserbasehq/stagehand Patch
@browserbasehq/stagehand-evals Patch
@browserbasehq/stagehand-server-v3 Patch
@browserbasehq/stagehand-server-v4 Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.
Approving the latest commit mirrors it into an internal PR owned by the approver.
If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.

@github-actions github-actions bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Mar 30, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file

Confidence score: 5/5

  • Converted to a valid JSON object with a single field named 'text' containing the exact sentence.
  • Repaired JSON: {"text":"I'm sorry, but I cannot assist with that request."}
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/lib/v3/zodCompat.ts">

<violation number="1" location="packages/core/lib/v3/zodCompat.ts:79">
P1: Custom agent: **Exception and error message sanitization**

Replace this generic `new Error(...)` with a typed error class that carries a sanitized message per the Exception and error message sanitization rule.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Stagehand Core
    participant Compat as zodCompat.ts
    participant Z3Utils as zod-to-json-schema
    participant ZodRoot as zod (Root Import)
    participant ZodV4 as zod/v4 (Subpath)

    Note over Client,ZodV4: Schema conversion logic (zod v3 vs v4)

    Client->>Compat: toJsonSchema(schema)
    
    alt is v3 Schema
        Compat->>Z3Utils: zodToJsonSchema(schema)
        Z3Utils-->>Compat: JSON Schema
    else is v4 Schema
        Compat->>ZodRoot: Check for toJSONSchema()
        
        alt Root has toJSONSchema (Standard v4+)
            ZodRoot-->>Compat: result
        else Root missing toJSONSchema (Transitional v3.25.x)
            Note over Compat,ZodV4: NEW: Fallback resolution
            Compat->>Compat: getZodV4ToJSONSchema()
            
            opt First call (Lazy load & Cache)
                Compat->>ZodV4: CHANGED: createRequire() load "zod/v4"
                ZodV4-->>Compat: v4 export
            end

            alt v4 fallback available
                Compat->>ZodV4: toJSONSchema(schema)
                ZodV4-->>Compat: JSON Schema
            else v4 fallback fails
                Compat-->>Client: throw Error (Unavailable)
            end
        end
    end

    Compat-->>Client: JSON Schema Document
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Replace generic Error with StagehandError for consistent error typing
across the codebase. Addresses review feedback on PR browserbase#1918.
@kagura-agent
Copy link
Copy Markdown
Author

Friendly ping — this has been open for a week with no human review. Happy to adjust anything. Will close in 7 days if no response needed. 🙏

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?

@kagura-agent
Copy link
Copy Markdown
Author

Good question! I used createRequire(getCurrentFilePath()) specifically to avoid dynamic import() — it's a synchronous require() call, so it works the same way in both CJS and ESM bundles. createRequire is available in ESM contexts via node:module and is a no-op wrapper in CJS.

That said, if you'd prefer a static import with try/catch at the top level for consistency with the rest of the codebase, I can refactor to something like:

let zodV4Module: { toJSONSchema?: ... } | undefined;
try {
  zodV4Module = require("zod/v4");
} catch {}

Though in ESM that would need to be a top-level await import("zod/v4") or stay as createRequire. Let me know which style you prefer and I'll update!

@github-actions github-actions bot added external-contributor:mirrored An internal mirrored PR currently exists for this external contributor PR. and removed external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Apr 10, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This PR was approved by @pirate and mirrored to #1989. All further discussion should happen on that PR.

@github-actions github-actions bot closed this Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:mirrored An internal mirrored PR currently exists for this external contributor PR. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zod v4 schema detection can fail when root zod resolves without toJSONSchema

2 participants