fix(zod-v3): array .min and parent .refine no longer break per-field revalidation#183
Merged
ozzyfromspace merged 1 commit intomainfrom May 9, 2026
Merged
Conversation
The v3 adapter's `validateAtPath` previously walked a slim-and-
stripped derivative of the user's schema, which (a) re-created
`ZodArray` nodes without their `.min/.max/.length/.nonempty`
checks and (b) didn't peel `ZodEffects` (`.refine` / `.superRefine`)
at the root, so the walker bailed out and returned a "path not
found" stub. Two reported symptoms:
- **Bug 1** — `form.remove()` doesn't restore an array's
`.min(1)` error after the array empties. The slim-schema
re-creation drops the `.min` check, so re-validation at
the array path always reports success.
- **Bug 2** — when a parent object carries a `.refine()`,
deleting a leaf field doesn't restore the leaf's `.min(1)`
error. The walker can't descend through the root
`ZodEffects`, so per-field re-validation reports a path-
not-found stub instead of the leaf's own check.
Fix lives entirely in the v3 adapter:
- `getNestedZodSchemasAtPath` now peels transparent wrappers
(optional / nullable / default / effects / pipeline /
readonly / branded) BEFORE the kind check inside the
descent loop, so structural descent works regardless of
which wrapper sits between the parent and the target.
The peel is applied only when stepping into a child;
the schema returned at the target segment keeps its own
wrapper so parse-time semantics (Optional admits
`undefined`, Default substitutes, Effects runs the
predicate) are preserved.
- `nestedSchemasAtPath` (inside `validateAtPath`) walks the
ORIGINAL schema directly. The slim-schema pipeline is
appropriate for default-value derivation but strips the
very checks per-field re-validation needs to surface.
The v4 adapter already handles both cases correctly; tests
cover both adapters to lock in regression coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A consumer dogfooding 0.16.3 hit two related re-validation bugs in the v3 adapter (bug report):
form.remove()doesn't restore an array's.min(1)error when the array empties. The slim-schema pipeline used insidevalidateAtPathre-createdZodArraynodes without their.min/.max/.length/.nonemptychecks, so post-remove validation always reported success..refine()on a parent object (ZodEffects(ZodObject)), clearing a leaf field doesn't restore the leaf's.min(1)error. The path walker had no rule forZodEffectsand bailed out, so per-field re-validation reported a "path not found" stub instead of the leaf's own check.Both share one root cause: the v3 adapter's
validateAtPathwalked a slim-and-stripped derivative of the user's schema. The v4 adapter already handles both cases correctly.Fix
Two surgical changes in
src/runtime/adapters/zod-v3/index.ts:getNestedZodSchemasAtPathpeels transparent wrappers (optional / nullable / default / effects / pipeline / readonly / branded) before the kind check inside the descent loop, so structural descent works regardless of which wrapper sits between the parent and the target. The peel is applied only when stepping into a child; the schema returned at the target segment keeps its own wrapper so parse-time semantics are preserved.nestedSchemasAtPath(insidevalidateAtPath) walks the ORIGINAL schema directly. The slim-schema pipeline is appropriate for default-value derivation but strips the very checks per-field re-validation needs to surface.Test plan
test/composables/ancestor-revalidation.test.ts— both adapters, both bugs (4 tests; v3 versions failed before the fix, v4 versions passed unchanged)pnpm test— 2092 / 2092 passpnpm typecheckcleanpnpm lintcleanpnpm check:size— within limitspnpm check:bench— within ratio floorpnpm check:coverage— above thresholds (lines 89.63%, branches 79.41%, functions 86.15%, statements 85.57%)🤖 Generated with Claude Code