You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec,rest,cli): validation diagnostics reach the real defect — named view-union branches + invalid_key/invalid_element descent (#7025) (#7042)
* fix(spec,rest,cli): validation diagnostics reach the real defect (#6391, #5389)
Sweep #7025 — two cases where a refusal is correct but its DIAGNOSTIC cannot
reach the element that actually failed. Both fixes move the diagnostic face
only; the acceptance face is pinned unmoved in both directions.
#6391 — ViewMetadataSchema's union members are contractual, not positional.
Three of its four members were inline expressions with no name, so a consumer
diagnosing a failure could only reach a branch by indexing the nested
invalid_union `errors[]` BY MEMBER POSITION (objectui#3624 shipped exactly that
and had to hold the coupling down with a canary test). The union is now BUILT
from a named record — VIEW_METADATA_BRANCHES / VIEW_METADATA_MEMBERS — so
"member N is the published schema" is one declaration rather than two that can
drift, and diagnoseViewMetadata() returns the failing branch by NAME with that
branch's own leaf issues and real field paths.
The union is deliberately NOT converted to z.discriminatedUnion: that would
move membership (a discriminated union refuses an unknown discriminant outright
where this one falls through all four members, and several of these shapes
carry no discriminant at all). The dispatch is diagnostic — ViewMetadataSchema
remains the only judge of acceptance, and a pin asserts the two never disagree.
Measured: a 41-body corpus run through ViewMetadataSchema before and after
produces byte-identical verdicts, parse output and issue-code sets
(sha256 fca9df8937bbb9f736f11895a6e1ddf23b7fb25d9b13cfa7e67c71c8dfaaf2b2), and
that corpus is now a committed pin.
#5389 — invalid_key / invalid_element are descended, in all three consumers.
Zod hangs a failing record-key / map-element schema's real issues on
`issue.issues` — the same shape as invalid_union's `issue.errors`, one property
name over. The family had already been fixed three times for `errors` (#4971,
#5014, #5341) while none of the three consumers read `issues`, so both codes
surfaced as a bare wrapper line with the prescription stranded in the payload.
formatZodError/formatZodIssue (spec), zodIssuesToFields (the REST wire) and
formatZodErrors (the CLI terminal) now all descend it, additively: the
container's own line/entry is unchanged and the leaves follow it. Unlike a
union's branches — competing candidates, therefore ranked and capped — a
container's issues are the one list the inner schema produced, so every one of
them is reported.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQdfKG4YpSv1SkujpWmk2
* docs(spec): the acceptance-face corpus is 42 bodies, not 41
Comment-only. The pin table is 19 ACCEPT + 23 REFUSED = 42; the header said 41.
No assertion changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRQdfKG4YpSv1SkujpWmk2
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(spec,rest,cli): validation diagnostics reach the real defect — named view-union branches, and `invalid_key` / `invalid_element` descent (#6391, #5389)
8
+
9
+
Two cases where a refusal fired correctly but its *diagnostic* could not reach the
10
+
element that actually failed. Both fixes change the DIAGNOSTIC face only: every
11
+
input that parsed before parses after, every input refused before is refused
12
+
after, and each refusal keeps its issue codes (ADR-0112 / #6142 — a better
13
+
diagnostic never weakens the envelope). Pinned in both directions.
14
+
15
+
**#6391 — `ViewMetadataSchema`'s union members are now contractual.** Three of
16
+
its four members were inline expressions with no name, so a consumer diagnosing a
17
+
failure could only reach a branch by indexing the nested `invalid_union`
18
+
`errors[]`**by member position**; objectui shipped exactly that and had to hold
19
+
the coupling down with a canary test (objectui#3606 / PR objectui#3624). The
20
+
union is now built from a named record:
21
+
22
+
-`VIEW_METADATA_BRANCHES` — the branch names, in the union's own order;
23
+
-`VIEW_METADATA_MEMBERS` — branch name → the schema the union actually holds
24
+
(`viewItem` is identically `ViewItemWireSchema`, as before);
25
+
-`selectViewMetadataBranch(body)` — which branch a body claims, by the
26
+
discriminants the members already declare;
27
+
-`diagnoseViewMetadata(body)` — the failing branch **named**, with that branch's
28
+
own leaf issues and real field paths, so no consumer needs `errors[i]`.
29
+
30
+
The union is **not** converted to `z.discriminatedUnion`. That would move the
31
+
acceptance face — a discriminated union refuses an unknown discriminant outright
32
+
where this one falls through all four members, and several of these shapes carry
33
+
no discriminant at all. `ViewMetadataSchema` remains the only judge of
34
+
acceptance; the dispatch only explains a verdict it did not make, and a pin
35
+
asserts the two never disagree.
36
+
37
+
**#5389 — `invalid_key` / `invalid_element` are descended, in all three
38
+
consumers.** Zod hangs a failing record-key / map-element schema's real issues on
39
+
`issue.issues` — the same shape as `invalid_union`'s `issue.errors`, one property
40
+
name over. The family had already been fixed three times for `errors` (#4971,
41
+
#5014, #5341) and none of the three consumers read `issues`, so both codes
42
+
surfaced as a bare wrapper line with the prescription stranded in the payload.
43
+
Now `formatZodError`/`formatZodIssue` (spec), `zodIssuesToFields` (the REST wire)
44
+
and `formatZodErrors` (the CLI terminal) all descend it.
45
+
46
+
Before / after, a `z.record` with a constrained key:
47
+
48
+
```
49
+
✗ fields.First Name: Invalid key in record
50
+
```
51
+
```
52
+
✗ fields.First Name: Invalid key in record
53
+
✗ fields.First Name: Invalid identifier. Must be lowercase snake_case …
54
+
```
55
+
56
+
The expansion is strictly additive on every surface: the container's own line
57
+
(and, on the wire, its own `{field, code: 'invalid_shape', message}` entry) is
58
+
unchanged, and the leaves follow it. Unlike a union's branches — competing
59
+
candidates, therefore ranked and capped — a container's `issues` are the one list
60
+
the inner schema produced, so every one of them is reported.
0 commit comments