Skip to content

chore: upgrade to zod v4#4039

Open
carderne wants to merge 7 commits into
mainfrom
chore/zod-4
Open

chore: upgrade to zod v4#4039
carderne wants to merge 7 commits into
mainfrom
chore/zod-4

Conversation

@carderne

@carderne carderne commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Experimental, not thoroughly tested yet.

TODO list before merging

@changeset-bot

changeset-bot Bot commented Jun 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 822ea2f

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

This PR includes changesets to release 28 packages
Name Type
@trigger.dev/core Patch
@trigger.dev/sdk Patch
trigger.dev Patch
@trigger.dev/redis-worker Patch
@trigger.dev/schema-to-json Patch
@trigger.dev/build Patch
@trigger.dev/plugins Patch
@trigger.dev/python Patch
@internal/cache Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@trigger.dev/rbac Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@trigger.dev/sso Patch
@internal/testcontainers Patch
@internal/tracing Patch
@internal/tsql Patch
@internal/zod-worker Patch
@internal/dashboard-agent Patch
@internal/sdk-compat-tests Patch
@trigger.dev/react-hooks Patch
@trigger.dev/rsc Patch
@trigger.dev/database Patch
@trigger.dev/otlp-importer 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

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Package manifests and overrides update Zod versions, peer ranges, and dependency placement across the workspace. Core and webapp schemas switch to explicit string-keyed records and a few field-shape changes. Webapp routes and components migrate to parseWithZod, getFormProps, and .errors-based rendering. Several helpers and runtime paths add casts or updated type aliases to match Zod v4 APIs.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description is far from the required template and only notes the change is experimental with a TODO list. Add the Closes line, complete the checklist, and fill in Testing, Changelog, and Screenshots sections per the repository template.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately captures the main change: upgrading to Zod v4.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/zod-4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@4a249c1

trigger.dev

npm i https://pkg.pr.new/trigger.dev@4a249c1

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@4a249c1

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@4a249c1

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@4a249c1

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@4a249c1

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@4a249c1

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@4a249c1

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@4a249c1

commit: 4a249c1

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/triggerdotdev/trigger.dev/issues/comments/4799766654","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/triggerdotdev/trigger.dev/pull/4039?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Repository UI\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `8fad56a2-0f47-4f9f-b9bc-59ba132bf08d`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between d565949205ab1fefbc2cbf8bf5ab826207f3192e and 3d5ccd22d7736c6a1545298e1da9626a721e55ac.\n> \n> </details>\n> \n> <details>\n> <summary>⛔ Files ignored due to path filters (1)</summary>\n> \n> * `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (82)</summary>\n> \n> * `.changeset/zod-4-support.md`\n> * `apps/supervisor/package.json`\n> * `apps/webapp/app/components/metrics/QueryWidget.tsx`\n> * `apps/webapp/app/models/orgIntegration.server.ts`\n> * `apps/webapp/app/models/vercelIntegration.server.ts`\n> * `apps/webapp/app/models/vercelSdkRecovery.server.ts`\n> * `apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts`\n> * `apps/webapp/app/presenters/v3/SpanPresenter.server.ts`\n> * `apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx`\n> * `apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx`\n> * `apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-alerts/route.tsx`\n> * `apps/webapp/app/routes/account._index/route.tsx`\n> * `apps/webapp/app/routes/account.tokens/route.tsx`\n> * `apps/webapp/app/routes/admin.feature-flags.tsx`\n> * `apps/webapp/app/routes/api.v1.plain.customer-cards.ts`\n> * `apps/webapp/app/routes/api.v1.prompts.$slug.ts`\n> * `apps/webapp/app/routes/api.v1.query.ts`\n> * `apps/webapp/app/routes/api.v1.schedules.ts`\n> * `apps/webapp/app/routes/resources.feedback.ts`\n> * `apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx`\n> * `apps/webapp/app/services/apiAuth.server.ts`\n> * `apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts`\n> * `apps/webapp/app/services/queryService.server.ts`\n> * `apps/webapp/app/services/routeBuilders/apiBuilder.server.ts`\n> * `apps/webapp/app/services/routeBuilders/dashboardBuilder.server.ts`\n> * `apps/webapp/app/services/routeBuilders/dashboardBuilder.ts`\n> * `apps/webapp/app/utils/json.ts`\n> * `apps/webapp/app/utils/timeGranularity.ts`\n> * `apps/webapp/app/v3/featureFlags.server.ts`\n> * `apps/webapp/app/v3/featureFlags.ts`\n> * `apps/webapp/app/v3/marqs/types.ts`\n> * `apps/webapp/app/v3/schedules.ts`\n> * `apps/webapp/app/v3/services/aiRunFilterService.server.ts`\n> * `apps/webapp/app/v3/services/replayTaskRun.server.ts`\n> * `apps/webapp/app/v3/utils/zodPubSub.server.ts`\n> * `apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts`\n> * `apps/webapp/package.json`\n> * `internal-packages/clickhouse/package.json`\n> * `internal-packages/clickhouse/src/client/tsql.ts`\n> * `internal-packages/clickhouse/src/tsqlFunctions.test.ts`\n> * `internal-packages/compute/package.json`\n> * `internal-packages/compute/src/types.ts`\n> * `internal-packages/emails/package.json`\n> * `internal-packages/run-engine/package.json`\n> * `internal-packages/run-engine/src/batch-queue/types.ts`\n> * `internal-packages/schedule-engine/package.json`\n> * `internal-packages/tsql/package.json`\n> * `internal-packages/zod-worker/package.json`\n> * `internal-packages/zod-worker/src/index.ts`\n> * `package.json`\n> * `packages/cli-v3/package.json`\n> * `packages/cli-v3/src/cli/common.ts`\n> * `packages/cli-v3/src/commands/analyze.ts`\n> * `packages/cli-v3/src/mcp/tools/agentChat.ts`\n> * `packages/cli-v3/src/utilities/configFiles.ts`\n> * `packages/core/package.json`\n> * `packages/core/src/schemas/eventFilter.ts`\n> * `packages/core/src/schemas/json.ts`\n> * `packages/core/src/v3/apiClient/index.ts`\n> * `packages/core/src/v3/runEngineWorker/supervisor/schemas.ts`\n> * `packages/core/src/v3/schemas/api.ts`\n> * `packages/core/src/v3/schemas/build.ts`\n> * `packages/core/src/v3/schemas/common.ts`\n> * `packages/core/src/v3/schemas/eventFilter.ts`\n> * `packages/core/src/v3/schemas/messages.ts`\n> * `packages/core/src/v3/schemas/openTelemetry.ts`\n> * `packages/core/src/v3/schemas/query.ts`\n> * `packages/core/src/v3/schemas/resources.ts`\n> * `packages/core/src/v3/schemas/runEngine.ts`\n> * `packages/core/src/v3/schemas/schemas.ts`\n> * `packages/core/src/v3/serverOnly/httpServer.ts`\n> * `packages/core/src/v3/types/tools.ts`\n> * `packages/core/src/v3/zodIpc.ts`\n> * `packages/core/src/v3/zodMessageHandler.ts`\n> * `packages/core/src/v3/zodNamespace.ts`\n> * `packages/core/src/v3/zodSocket.ts`\n> * `packages/redis-worker/package.json`\n> * `packages/redis-worker/src/mollifier/schemas.ts`\n> * `packages/redis-worker/src/queue.ts`\n> * `packages/redis-worker/src/worker.ts`\n> * `packages/schema-to-json/package.json`\n> * `packages/trigger-sdk/package.json`\n> \n> </details>\n> \n> \n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>📝 Generate docstrings</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> Create stacked PR\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> Commit on current branch\n\n</details>\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `chore/zod-4`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=triggerdotdev/trigger.dev&utm_content=4039)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

coderabbitai[bot]

This comment was marked as resolved.

carderne added 5 commits June 25, 2026 16:19
Migrate the dashboard form layer from @conform-to 0.9 to the 1.x API.
conform 1.x peer-depends on zod ^3.21 || ^4, so it runs on the current
zod 3 and unblocks the zod 4 upgrade (conform 0.9 cannot bundle against
zod 4, which removed ZodNativeEnum, ZodEffects, and ZodPipeline).
Address CodeRabbit feedback on the conform 1.x migration: use defaultValue
(not value) on hidden inputs so they stay uncontrolled, return the env-var
project-lookup failure as a form-level error, join multi-message field
errors when rendering, and clone alerts.emails before using it as mutable
field-list state.
The schedules and branches add-on purchase modals detected conform
responses with "intent" in fetcher.data, but conform v1's
submission.reply() returns a status field, not intent, so lastResult was
always undefined and server-side errors never rendered. Switch the guard
to "status" in fetcher.data, matching the rest of the migration.
- core: forward the validated/normalized payload (parsedPayload.data)
  from the zod message sender instead of casting the raw input, so zod
  transforms, defaults, and coercions are applied on send
- webapp: reword the feedback type union error to cover all feedback
  types, not just bug/feature
coderabbitai[bot]

This comment was marked as resolved.

The root @conform-to/zod entry is the zod 3 implementation (it imports
ZodBranded/ZodEffects from zod), so it throws at module load under
zod 4.4.3, breaking the auth/invite forms and the e2e suites. Switch all
44 imports to the @conform-to/zod/v4 subpath, which targets zod 4.

Also address review feedback:
- concurrency: use formEnvironments.errorId (not .id) for the FormError
  element id to avoid a duplicate DOM id and keep aria-describedby valid
- team / confirm-basic-details: pass value:false to getInputProps on the
  controlled number/hidden inputs so conform omits defaultValue and the
  inputs stay purely controlled
- branches.archive: join the form.errors string[] for readable output
@carderne carderne marked this pull request as ready for review June 25, 2026 18:37

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 6 potential issues.

Open in Devin Review

<Fieldset className="max-w-full gap-y-3">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<input value={location.pathname} {...getInputProps(fields.path, { type: "hidden" })} />
<InputGroup className="max-w-full">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Hidden form fields lose their explicit values, causing feedback submissions to redirect to an empty path

The page path for the feedback form is placed before the spread of auto-generated props (getInputProps(fields.path, { type: "hidden" }) at apps/webapp/app/components/Feedback.tsx:95), so the library's own value silently overrides it.

Impact: Feedback submissions will send an empty or stale path field instead of the current page URL, breaking the post-submit redirect.

Conform v1 getInputProps returns a value prop for hidden inputs that overrides the explicit one

In conform v0.9, conform.input(field, { type: "hidden" }) returned defaultValue, which doesn't conflict with an explicit value prop. In conform v1, getInputProps(field, { type: "hidden" }) returns a value prop (the field's current form-state value).

When the JSX is <input value={location.pathname} {...getInputProps(fields.path, { type: "hidden" })} />, the spread comes second and its value overwrites the explicit value={location.pathname}. Since no defaultValue is set in useForm for the path field, the form submits path="" instead of path="/current/page".

The correct pattern in conform v1 is shown in apps/webapp/app/routes/confirm-basic-details.tsx:315:

<input {...getInputProps(confirmEmail, { type: "hidden", value: false })} value={user.email} />

— either pass { value: false } to disable auto-value, or put the explicit value after the spread.

Suggested change
<InputGroup className="max-w-full">
<input {...getInputProps(fields.path, { type: "hidden", value: false })} value={location.pathname} />
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 966 to +972
<input
value={parentEnvironment.id}
{...conform.input(parentEnvironmentId, { type: "hidden" })}
{...getInputProps(parentEnvironmentId, { type: "hidden" })}
/>
<input
value={location.pathname}
{...conform.input(failurePath, { type: "hidden" })}
{...getInputProps(failurePath, { type: "hidden" })}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Hidden form fields lose their explicit values, preventing new branches from being created with the correct parent environment

The parent environment ID and failure redirect path are placed before the spread of auto-generated props (getInputProps(parentEnvironmentId, { type: "hidden" }) at apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx:966-972), so the library's own empty value silently overrides them.

Impact: The create-branch form submits empty values for environment ID and redirect path, causing branch creation to fail or redirect incorrectly.

Same getInputProps hidden-input override as BUG-0001

In conform v1, getInputProps(field, { type: "hidden" }) returns a value prop. When the JSX places the explicit value BEFORE the spread, the spread's value overrides it.

Lines 966-972:

<input value={parentEnvironment.id}
  {...getInputProps(parentEnvironmentId, { type: "hidden" })} />
<input value={location.pathname}
  {...getInputProps(failurePath, { type: "hidden" })} />

Both need either { value: false } in the options or the explicit value placed after the spread.

Suggested change
<input
value={parentEnvironment.id}
{...conform.input(parentEnvironmentId, { type: "hidden" })}
{...getInputProps(parentEnvironmentId, { type: "hidden" })}
/>
<input
value={location.pathname}
{...conform.input(failurePath, { type: "hidden" })}
{...getInputProps(failurePath, { type: "hidden" })}
<input
{...getInputProps(parentEnvironmentId, { type: "hidden", value: false })}
value={parentEnvironment.id}
/>
<input
{...getInputProps(failurePath, { type: "hidden", value: false })}
value={location.pathname}
/>
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +98 to 102
<input value={environment.id} {...getInputProps(environmentId, { type: "hidden" })} />
<input
value={`${location.pathname}${location.search}`}
{...conform.input(redirectPath, { type: "hidden" })}
{...getInputProps(redirectPath, { type: "hidden" })}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Hidden form fields lose their explicit values, causing branch archive to fail or redirect to a wrong page

The environment ID and redirect path are placed before the spread of auto-generated props (getInputProps(environmentId, { type: "hidden" }) at apps/webapp/app/routes/resources.branches.archive.tsx:98-102), so the library's own empty value silently overrides them.

Impact: Archiving a branch submits wrong field values, causing the archive action to fail or redirect incorrectly after completion.

Same getInputProps hidden-input override as BUG-0001

Lines 98-102:

<input value={environment.id} {...getInputProps(environmentId, { type: "hidden" })} />
<input value={`${location.pathname}${location.search}`}
  {...getInputProps(redirectPath, { type: "hidden" })} />

Both need either { value: false } in the options or the explicit value placed after the spread.

Suggested change
<input value={environment.id} {...getInputProps(environmentId, { type: "hidden" })} />
<input
value={`${location.pathname}${location.search}`}
{...conform.input(redirectPath, { type: "hidden" })}
{...getInputProps(redirectPath, { type: "hidden" })}
/>
<input {...getInputProps(environmentId, { type: "hidden", value: false })} value={environment.id} />
<input
{...getInputProps(redirectPath, { type: "hidden", value: false })}
value={`${location.pathname}${location.search}`}
/>
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

</InputGroup>
{canCreateV3Projects ? (
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v3"} />
<input {...getInputProps(projectVersion, { type: "hidden" })} defaultValue={"v3"} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Project version hidden field uses defaultValue which is ignored, so new projects may be created without the correct version

The project version hidden input uses defaultValue after the spread (getInputProps(projectVersion, { type: "hidden" }) at apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx:391), but the library returns a value prop for hidden inputs, so React ignores defaultValue entirely.

Impact: New projects may be created without a version field, because the hidden input submits the field's empty form-state value instead of "v3" or "v2".

getInputProps for hidden type returns value, making defaultValue a no-op

In conform v1, getInputProps(field, { type: "hidden" }) returns a value prop from the field's form state. React ignores defaultValue when value is present on the same element.

Old code (worked because conform.input returned defaultValue, not value):

<input {...conform.input(projectVersion, { type: "hidden" })} value={"v3"} />

New code (broken because getInputProps returns value, and defaultValue is ignored):

<input {...getInputProps(projectVersion, { type: "hidden" })} defaultValue={"v3"} />

Fix: use value after the spread, or pass { value: false } to opt out of auto-value.

Suggested change
<input {...getInputProps(projectVersion, { type: "hidden" })} defaultValue={"v3"} />
<input {...getInputProps(projectVersion, { type: "hidden", value: false })} value={"v3"} />
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +99 to 123
export function getFlagControlType(schema: z.ZodType): FlagControlType {
// zod v4: schema.def.type is a lowercase tag ("boolean"/"enum"/"number"/...).
const def = schema.def as {
type: string;
checks?: Array<{ _zod?: { def?: { check?: string; value?: number } } }>;
};

if (typeName === "ZodBoolean") {
if (def.type === "boolean") {
return { type: "boolean" };
}

if (typeName === "ZodEnum") {
return { type: "enum", options: schema._def.values as string[] };
if (def.type === "enum") {
return { type: "enum", options: (schema as z.ZodEnum).options as string[] };
}

// z.coerce.number() reports as ZodNumber; pull min/max out of its checks
// so the UI can render a constrained number input instead of free text.
if (typeName === "ZodNumber") {
const checks = (schema._def.checks ?? []) as Array<{ kind: string; value?: number }>;
const min = checks.find((c) => c.kind === "min")?.value;
const max = checks.find((c) => c.kind === "max")?.value;
// z.coerce.number() reports as "number"; pull min/max out of its v4 checks
// (each check carries `_zod.def.{check,value}`) so the UI can render a
// constrained number input instead of free text.
if (def.type === "number") {
const checks = def.checks ?? [];
const min = checks.find((c) => c._zod?.def?.check === "greater_than")?._zod?.def?.value;
const max = checks.find((c) => c._zod?.def?.check === "less_than")?._zod?.def?.value;
return { type: "number", min, max };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 getFlagControlType relies on undocumented zod v4 internals for check introspection

The rewritten getFlagControlType casts schema.def and inspects _zod.def.check strings like "greater_than" and "less_than" to extract min/max bounds from number schemas (apps/webapp/app/v3/featureFlags.ts:117-121). This relies on zod v4's internal check structure, which is not part of the public API. If zod v4 uses different check names (e.g., "minimum" for .min() and "maximum" for .max()), the bounds would silently resolve to undefined and the admin flag UI would show an unconstrained number input instead of 0–100. Similarly, z.coerce.boolean() and z.coerce.number() may report a different def.type than plain z.boolean() / z.number(), causing those flags to fall through to the "string" default. I could not verify the exact v4 internal names without running the code.

(Refers to lines 99-125)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +281 to 283
// send the validated/normalized output so zod transforms, defaults, and coercions are applied
await this.#sender({ type, payload: parsedPayload.data, version: "v1" });
} catch (error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 ZodMessageSender.send() now sends parsedPayload.data instead of the raw payload

In packages/core/src/v3/zodMessageHandler.ts:280-283, send() was changed from sending the raw payload to sending parsedPayload.data. This means zod transforms, defaults, and coercions are now applied before the message is sent over the wire. This is a semantic change: previously, receivers got the raw caller input; now they get the parsed/transformed output. The same change appears at line 336. This could change wire-format for messages that use transforms in their schemas. The comment says this is intentional ("send the validated/normalized output so zod transforms, defaults, and coercions are applied"). If any receiver re-parses with the same schema, the double-transform could produce different results (e.g., a coerce transform applied twice).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant