Skip to content

fix(zod-v3): array .min and parent .refine no longer break per-field revalidation#183

Merged
ozzyfromspace merged 1 commit intomainfrom
claude/array-and-refine-revalidation
May 9, 2026
Merged

fix(zod-v3): array .min and parent .refine no longer break per-field revalidation#183
ozzyfromspace merged 1 commit intomainfrom
claude/array-and-refine-revalidation

Conversation

@ozzyfromspace
Copy link
Copy Markdown
Contributor

Summary

A consumer dogfooding 0.16.3 hit two related re-validation bugs in the v3 adapter (bug report):

  • Bug 1form.remove() doesn't restore an array's .min(1) error when the array empties. The slim-schema pipeline used inside validateAtPath re-created ZodArray nodes without their .min/.max/.length/.nonempty checks, so post-remove validation always reported success.
  • Bug 2 — with .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 for ZodEffects and 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 validateAtPath walked 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:

  • getNestedZodSchemasAtPath 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 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.

Test plan

  • Failing repros in 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 pass
  • pnpm typecheck clean
  • pnpm lint clean
  • pnpm check:size — within limits
  • pnpm check:bench — within ratio floor
  • pnpm check:coverage — above thresholds (lines 89.63%, branches 79.41%, functions 86.15%, statements 85.57%)

🤖 Generated with Claude Code

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
attaform Ready Ready Preview, Comment May 9, 2026 6:25am

@ozzyfromspace ozzyfromspace merged commit aa0790b into main May 9, 2026
12 checks passed
@ozzyfromspace ozzyfromspace deleted the claude/array-and-refine-revalidation branch May 9, 2026 06:32
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