Skip to content

fix(rest): zodIssuesToFields 展开 invalid_union,联合分支里的处方到达调用方 (#5014) - #5362

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5014-rest-union-fields
Aug 5, 2026
Merged

fix(rest): zodIssuesToFields 展开 invalid_union,联合分支里的处方到达调用方 (#5014)#5362
os-zhuang merged 2 commits into
mainfrom
claude/issue-5014-rest-union-fields

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5014

机制

zod 把一个失配的 z.union([...]) 折叠成一条顶层 invalid_union issue,它自己的 message 是裸的 "Invalid input";每个分支真正的抱怨——包括 #4001 那批 strictObject 写下的处方文案——躺在 issue.errors 里(每分支一个数组)。packages/rest/src/rest-server.tszodIssuesToFields 过去只映射顶层 issue,分支错误整体丢失。

前提核验(issue body 是线索,不是规格)

机制成立,但 issue 里那个具体 witness 已经过期,两点分别记录:

另需说明:元数据发布路径(DashboardWidgetSchema 这类)并不经过 zodIssuesToFields,它走 sendErrorstatus 直通并原样透传 issues;那是另一个面,不在本单文件面内。

修法:照抄 #4971 的分支选择策略,不重新发明

selectUnionBranches / renderIssue 已于 546ab3c49(PR #5342)为 spec/CLI 侧的 formatZodError 落地。本 PR 复用同一套策略,逐条对齐:

  1. 只报根部 KIND 不匹配的分支(expected string, received object)整支丢弃;全部如此则不展开,输出与从前逐字一致。
  2. 报得最少的分支胜出——这条就是「一个拼错的键被 N 个分支各报一遍」(未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 批 6c 回归)的防御机制本身。
  3. unrecognized_keys 破平局(策展文案在那儿)。
  4. 声明顺序破其余;真正并列的分支全部输出,上限 3;跨分支相同结论只出现一次;嵌套 union 按绝对路径递归,深度上限 3。

复用方式说明(三消费者必须同判定):代码是改写(adapt)而非 import。spec 只导出字符串渲染器(formatZodIssue / formatZodError),而 wire 需要的是结构化的 {field, code, message} 条目;把选择逻辑单独导出需要改 packages/spec,本单明确不碰。所以策略在 rest-server.ts 里以同名函数、同顺序、同上限重写,并在注释里指回 spec 的出处。CLI 侧的 formatZodErrors(#5341)还未做——三处必须给出同一个判定,否则同一个错误从终端发布和从 API 提交会得到两套说法。

一处有意的分歧(只在渲染,不在判定):spec 侧在并列分支超限时打印一行 … and N more branches rejected this value;fields[] 不能有这种条目——一条 field error 必须指向真实字段并带目录码,这行两样都没有,故不输出。

wire 契约(ADR-0114)

改动是纯追加:

  • 原有每一条 fields[] 条目(包括联合自身那条 invalid_shape / "Invalid input")的 fieldcodemessage 与相对次序完全不变;新条目插在它解释的那条之后。
  • 信封形状仍与 mapDataErrorVALIDATION_FAILED 同形({field, code, message});数组长度从来不是契约的一部分,ADR-0114 约束的是 code 取值必须落在 FieldErrorCode 目录内——新增条目同样走 zodIssueToFieldCode,并有测试逐条断言其为目录成员。
  • 保留联合自身那条而不是替换掉它,有三个理由:它是唯一指名「客户端发来的那个槽位」的条目;现有客户端已经在读它;当所有分支都不 informative 时它就是全部答案。
  • 一个必须点名的细节:分支 issue 的 path相对于联合的,而 ADR-0114 D3 用「把 path 走进入参」来区分 requiredinvalid_type。所以 zodIssueToFieldCode 改成接收绝对路径;沿用相对路径会读到错误的槽位(通常是 undefined),把每个分支的类型不符都误报成 required

验证

先红后绿 + 反向验证,三步都在报告里留了真实输出:

  • 先红:新测试文件对未改动的实现跑 —— 10 failed | 3 passed,失败信息就是 [{"field":"query.search","code":"invalid_shape","message":"Invalid input"}]: expected undefined to be defined。通过的那 3 条正是不变量(全 KIND 不匹配不展开 / 非 union 一对一 / junk 容错)。
  • 绿:packages/rest 全量 41 test files | 621 tests passed
  • 反向验证(方向事先声明):把 selectUnionBranches 临时改成「展开所有分支」,预期是防噪测试转红——实测一个 typo 从 2 条变成 7 条条目、其中 unknown_field 出现 2 次,同时 KIND 不匹配不变量、上限、平局三条测试一起转红。随后已还原。

Generated by Claude Code

zod 把失配的 z.union 折叠成一条顶层 invalid_union issue,message 是裸的
"Invalid input",各分支真正的拒绝理由(含 #4001 那批 strictObject 处方)
挂在 issue.errors 里。zodIssuesToFields 只映射顶层 issue,所以
POST /api/v1/data/:object/query 对 {"search":{"fields":["name"]}} 只回
{field:"query.search", code:"invalid_shape", message:"Invalid input"} ——
「缺 query 这个键」那句话被生产出来又被丢掉。

现在联合条目之后追加解释它的分支条目:field 用分支相对路径拼上联合自身路径,
code 仍走 ADR-0114 D3 的目录映射(缺键 → required,这需要绝对路径去读入参)。

分支选择策略沿用 #4971 给 spec 侧 formatZodError 落的那一套(丢弃只报根部
KIND 不匹配的分支;报得最少的分支胜出——这条就是防止一个未知键被 N 个分支各
报一遍的机制;unrecognized_keys 破平局;声明顺序破其余;并列全出,上限 3;
跨分支重复结论只出现一次;嵌套联合按绝对路径递归,深度上限 3),两侧判定必须
一致,否则同一个错误从终端和从 API 会得到两套说法。

对 wire 是纯追加:原有条目的 field/code/message 与相对次序均不变。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTszibd6C8sUCCZnM4VcrL
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 1:11am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v17.mdx (via @objectstack/rest)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

联合失配现在除了联合自身那条,还会追加解释它的分支条目,所以
`fields.length` 要读作「字段错误条数」而不是「zod issue 条数」。
这段示例正是对外推荐 `zodIssuesToFields` 的地方。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTszibd6C8sUCCZnM4VcrL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

union 分支里的 unknown-key 处方永远到不了作者:zodIssuesToFields 只映射顶层 issue,失败的 union 只剩 Invalid input

2 participants