Skip to content

Commit c15eb23

Browse files
hotlongclaude
andauthored
fix(metadata-protocol): the 422 issues[] descends invalid_key / invalid_element, so a rejected record key arrives with the rule it broke (#8817)
* fix(metadata-protocol): descend invalid_key / invalid_element in the 422 issues[] (#8783) zodIssuesToMetadataIssues expanded invalid_union only, so a z.record/z.map key rejection reached Studio as the bare wrapper "Invalid key in record" while the rule it broke sat one level down in issue.issues — the shape #4971 named, and the one both packages/spec walks have descended since #5389. ObjectSchema.fields is a constrained-key record carrying the snake_case rule and object is in the builtin registry, so this was the commonest authoring mistake on the most-authored metadata type. The descent is additive and matches what the two spec walks were measured to emit for the card's own repro: the wrapper entry stays at index 0, the detail joins it on the same slot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8 * test(metadata-protocol): pin the container descent across all three walks + changeset (#8783) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b705a6c commit c15eb23

4 files changed

Lines changed: 603 additions & 21 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata): the `422 INVALID_METADATA` envelope descends `invalid_key` / `invalid_element`, so a rejected record key arrives with the rule it broke (#8783)
6+
7+
Zod raises a `z.record` / `z.map` **key** rejection as `invalid_key` and a
8+
`z.map` **element** rejection as `invalid_element`, and in both cases the
9+
issue's own `message` is a bare wrapper — `"Invalid key in record"` — with the
10+
real diagnosis one level down in `issue.issues`. That is structurally the
11+
`invalid_union` shape #4971 named: the prescription is produced and then
12+
dropped by a walk that reads only the top level.
13+
14+
Both `packages/spec` walks learned to descend those codes in #5389.
15+
`zodIssuesToMetadataIssues` — the walk behind `saveMetaItem`'s 422 (#5364) and
16+
the read path's diagnostics (#5598) — expanded `invalid_union` only, so it
17+
stopped at the wrapper. Three walks over one `safeParse`, two of them reaching
18+
the prescription and the Studio-facing one not.
19+
20+
**It was reachable from ordinary authored metadata, not synthetic.**
21+
`ObjectSchema.fields` is a record whose KEY schema carries the snake_case rule
22+
(`spec/src/data/object.zod.ts`), and `object` is in the builtin
23+
`getMetadataTypeSchema` registry. So the commonest authoring mistake on the
24+
most-authored metadata type — writing `firstName` for a field key, which is
25+
exactly what an agent coming from JS naming writes — produced:
26+
27+
```
28+
{ path: 'fields.firstName', code: 'invalid_key', message: 'Invalid key in record' }
29+
```
30+
31+
The author was told a key was invalid and never told what a valid one looks
32+
like, so the next move was to guess. The declared message existed and was
33+
correct; it just did not reach anyone. Now the same save answers:
34+
35+
```
36+
{ path: 'fields.firstName', code: 'invalid_key', message: 'Invalid key in record' }
37+
{ path: 'fields.firstName', code: 'invalid_format', message: 'Field names must be lowercase snake_case (e.g., "first_name", …)' }
38+
```
39+
40+
**Additive, and matched to the walks that already worked** rather than chosen.
41+
The other two were measured over the card's own repro first: `formatZodIssue`
42+
prints the wrapper line then the indented detail, and `zodIssuesToFields` emits
43+
the `invalid_shape` wrapper entry then the detail entry. So the wrapper stays at
44+
index 0 — it is the only entry naming the slot the client sent, and Studio's
45+
designer keys on it — and the detail joins it on the same path. No entry that
46+
shipped before is removed or renumbered.
47+
48+
**Targeted, not a widened walk.** Only the two container codes open the descent;
49+
an `issues` array hanging off any other code is still ignored, `invalid_union`
50+
still expands through the unchanged ranking, and the nesting bound now covers
51+
both descents at the same depth of 3. Container issues are deliberately *not*
52+
ranked the way union branches are: a union's branches are competing candidates,
53+
while a container has one inner schema, so every issue it raised is a true
54+
statement about the value.
55+
56+
The verdict is unchanged in every case — this moves what a refusal *says*, never
57+
whether it is one. `union-branch-policy.cross-package-parity.test.ts` gains a §5
58+
comparing the container descent across all three walks; its §1 (the policy is
59+
not publicly exported from `@objectstack/spec`, so this package must run its own
60+
copy) is untouched, and no export was added.

0 commit comments

Comments
 (0)