Skip to content

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

Description

@xuyushun441-sys

#4001 战役的每一条策展处方,只要它所在的 schema 被包在一个 z.union([...]) 分支里,在线上就是不可见的。测量于 2026-08-03(批 14 收紧 DashboardWidget.compareTo 时,先写断言再证红,发现断言红在了意料之外的地方)。

机制

zod 把一个失配的 union 折叠成一条顶层 invalid_union issue,message 是裸的 Invalid input;各分支的真实错误挂在 issue.errors(每个分支一个数组)。

packages/rest/src/rest-server.tszodIssuesToFields 是:

return issues.map((i: any) => ({
    field: ..., code: zodIssueToFieldCode(i, ...), message: String(i?.message ?? 'Invalid value'),
}));

—— 只走顶层。invalid_union 被映射成 code: 'invalid_shape',message: 'Invalid input',issue.errors 里的内容整个丢掉

实测

DashboardWidgetSchema.parse({ id, dataset, values, compareTo: { offset: '7d', granularity: 'month' } }):

顶层:

code=invalid_union path=["compareTo"] msg=Invalid input

而分支错误里躺着这条(批 14 新写的):

Unrecognized key(s) on this comparison window: `granularity`. Until #4001 closed this shape
these were dropped silently — the dashboard still rendered, without whatever the key was
meant to configure.

作者看到的是 Invalid input

为什么这是战役级问题而不是一个 widget 的问题

拒绝本身是对的、也是 #4001 的收益(静默半丢弃 -> 硬失败)。问题是处方这一半:账本 finding 18 已经把它写成规则 —— 「prose in a rejection is behaviour, not documentation」。战役花在 aliases/guidance 上的策展,一旦落在 union 分支内,就等于没写;而且没有任何测试会发现,因为对着 schema 断言 message 是能过的(错误确实被产生了,只是没被传递)。

需要一次普查:packages/spec 里有多少条 strictObject( 落在 union 分支内(compareTosubmitBehaviorview.zod 的若干 union 成员等)。

建议

zodIssuesToFields 里展开 invalid_union:把每个分支的子 issue 提升为独立的 field 条目(path 需要拼上父 path),或者在只有一个分支「明显是作者意图的那支」时优先提升该分支。后者更难判定但输出更干净 —— 对象输入配对象分支,是一个可用的启发。

⚠️ 这是 wire 契约的改动(fields[] 的条数会变),按 ADR-0114 需要和 mapDataError 保持同形。

批 14 已在 dashboard.zod.tscompareTo 处记下这条实测限制,并在测试里分开 pin 了两件事:作者今天真正看到的裸 message,和等着被传递的处方文案 —— 免得一个绿测试冒充一条没人打印的消息。

发现于 #4001 批 14 的范围外体检,未指派。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions