Skip to content

feat: allow 'when' parameter in .superRefine#5741

Merged
colinhacks merged 2 commits into
colinhacks:mainfrom
vilvai:main
Apr 27, 2026
Merged

feat: allow 'when' parameter in .superRefine#5741
colinhacks merged 2 commits into
colinhacks:mainfrom
vilvai:main

Conversation

@vilvai

@vilvai vilvai commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

This PR adds the params object to .superRefine which allows using when parameter to run the superrefine validation even when the base schema fails, similar to how .refine works currently

See my issue about this: #5739

@vercel

vercel Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

@vilvai is attempting to deploy a commit to the colinhacks Team on Vercel.

A member of the Team first needs to authorize it.

@vilvai vilvai changed the title allow 'when' parameter in .superRefine feat: allow 'when' parameter in .superRefine Mar 2, 2026
@pullfrog

pullfrog Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

TL;DR — Adds an optional params argument to .superRefine() (matching the existing .refine() API) so that a when callback can control whether the refinement runs — most notably allowing super-refinements to execute even when the base schema has errors.

Key changes

  • Add $ZodSuperRefineParams interface to core — introduces a when option for _superRefine, forwarding it to the underlying _check call
  • Thread params through classic and mini APIs — both superRefine() helpers and the ZodType.superRefine method now accept and forward the optional params object
  • Improve when JSDoc in $ZodCheckDef — clarifies the doc comment to match the new $ZodSuperRefineParams wording
  • Add tests for when in .superRefine() — two tests verify that a when callback controls whether the refinement runs when the base schema has errors

Summary | 5 files | 2 commits | base: mainmain

Before: .superRefine() accepted only a refinement function and always used the default when behavior (skip when aborted), meaning it could never run when the base schema produced errors.
After: .superRefine() accepts an optional second argument ({ when? }) that lets callers override the default gating — e.g. using a partial safeParse in when to run cross-field refinements even when some fields fail.

The new $ZodSuperRefineParams interface intentionally exposes only when (not message, path, abort, etc.) because superRefine pushes issues manually via ctx.addIssue. The params object is forwarded to _check which already supports these options, so the implementation is a thin passthrough.

How does the when callback work?

when receives the parse payload and returns a boolean. When it returns true the refinement executes regardless of earlier failures. A common pattern is to partially validate the input (e.g. baseSchema.pick({ foo: true }).safeParse(value).success) so the refinement only runs when the fields it depends on are valid.

api.ts · checks.ts · classic/schemas.ts · mini/schemas.ts · refine.test.ts

Pullfrog  | View workflow run | via Pullfrog | Using Claude Opus𝕏

@pullfrog pullfrog 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.

Important

The JSDoc on $ZodSuperRefineParams.when describes the default behavior incorrectly — worth fixing before merge.

  • Added $ZodSuperRefineParams interface with a when option and threaded it through _superRefine_check in both classic and mini APIs
  • Added two tests covering the when: () => true (superRefine runs despite base schema failure) and when: () => false (superRefine skipped) cases

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread packages/zod/src/v4/core/api.ts Outdated
* `when` — whether to run the refinement at all.
*/
export interface $ZodSuperRefineParams {
/** If provided, this refinement will only be executed if the function returns `true`. Defaults to `payload => z.util.isAborted(payload)`. */

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.

The JSDoc says the default is payload => z.util.isAborted(payload), but the actual default behavior (when when is omitted) is to skip the check when the payload is aborted — effectively the opposite. Additionally, z.util.isAborted doesn't exist in v4; the internal helper is util.aborted.

Note: this same misleading JSDoc exists on $ZodCheckDef.when in checks.ts:16, so it's a pre-existing issue that was copied here. Still, it would be good to fix it in this PR since it's in newly added code.

Suggested wording:

/** If provided, this refinement will only be executed when the function returns `true`. When omitted, the refinement is skipped if the base schema produced any aborting issues. */
Suggested change
/** If provided, this refinement will only be executed if the function returns `true`. Defaults to `payload => z.util.isAborted(payload)`. */
/** If provided, this refinement will only be executed when the function returns `true`. When omitted, the refinement is skipped if the base schema produced any aborting issues. */

The previous `string | $ZodCustomParams` type advertised fields that
don't do anything useful for `.superRefine` — `error`/`message`/`path`
are never read because the refinement pushes its own issues via
`ctx.addIssue`, and `abort` is redundant with the per-issue `fatal`
flag (which sets `continue: false` directly on the issue, with finer
granularity than a check-level toggle).

Introduce a dedicated `$ZodSuperRefineParams` interface exposing only
`when` — the one execution-control option that genuinely gates whether
the refinement runs at all. Also wire `params` through in `mini` for
parity.
@colinhacks colinhacks merged commit 65fab33 into colinhacks:main Apr 27, 2026
4 of 7 checks passed

@pullfrog pullfrog 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.

No new issues. Reviewed the following changes:

  • Fixed the JSDoc on $ZodSuperRefineParams.when to accurately describe the default behavior
  • Fixed the same misleading JSDoc on $ZodCheckDef.when in checks.ts (the pre-existing issue flagged in the prior review)
  • Removed the verbose block comment on the $ZodSuperRefineParams interface

Prior review feedback:

  • JSDoc on $ZodSuperRefineParams.when describes default behavior incorrectly — addressed (also fixed in $ZodCheckDef.when)

Pullfrog  | View workflow run | Using Claude Opus𝕏

@pullfrog pullfrog 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.

Approving — prior JSDoc feedback was fully addressed and no new issues found.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@colinhacks

Copy link
Copy Markdown
Owner

Narrowed the params type on .superRefine to just { when } for now, rather than the full .refine params shape — when is really the only one that makes sense here, since the refinement pushes its own issues via ctx.addIssue (so error/message/path aren't read) and abort is redundant with the per-issue fatal flag. Thanks for picking this up 👍

Note: this comment was produced by an AI coding assistant.

@colinhacks

Copy link
Copy Markdown
Owner

Landed in Zod 4.4

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.

2 participants