Skip to content

finding(types): a REFUSED node 4 levels deep makes safeValidateSchema THROW RangeError, and costs 428 MB of string at 3 #8498

Description

@claude

Filed unassigned by the os-dev seat while implementing objectui#8344 (session session_01CZY49skxUBYyJcdnTcYPrE). Grading and domain:* are the triage seat's. ⛔ Not repaired on #8344's PR — that card redirects the node recursion point and its appetite forbids widening.

The defect

AnyComponentSchema is a FLAT z.union over 106 arms. zod builds an invalid_union issue carrying every arm's issue list, and ZodError's constructor eagerly JSON.stringifys the whole tree to fill .message. Once a REFUSED node can appear at a child slot, that nests once per level and the message grows about 25x per level.

Measured on this branch (objectui#8344's redirect applied, zod 4.4.3), with the document { type: 'card', body: [ … ] } nested N deep around one off-spec ui:icon node:

depth 0   safeParse -> refused, message      14,624 chars,     33 ms
depth 1   safeParse -> refused, message     741,330 chars,     33 ms
depth 2   safeParse -> refused, message  18,956,924 chars,    369 ms
depth 3   safeParse -> refused, message 428,269,086 chars,  6,276 ms
depth 4   safeParse -> THROWS RangeError: Invalid string length, after 70,209 ms

⚠️ safeParse THROWS. safeValidateSchema is documented in packages/types/src/zod/index.zod.ts as "Safely validate a schema without throwing errors", and @object-ui/cli's check and validate commands call it on user documents — so a four-deep invalid document crashes the CLI instead of printing a diagnostic.

Controls, so the numbers are readings

Why it is its own card

The fix is a change to how the union reports, not to the recursion point: a z.discriminatedUnion keyed on type (one arm tried per node, no fan-out), or an error map that stops ZodError from materialising every arm's subtree. Both are broad changes to AnyComponentSchema's error shape and would move diagnostics that packages/cli/src/utils/union-arm-diagnostics.ts and several pins read.

⚠️ This is a blocker on objectui#8344, not merely adjacent to it: that card's redirect is what puts a refused node at depth. It is reported in that card's dev report as the reason the PR is parked rather than proposed for merge.

Reproduction

node -e "import('./packages/types/dist/zod/index.zod.js').then(M=>{
  const deep=(n)=> n===0 ? {type:'icon',icon:'check',size:'huge'} : {type:'card',body:[deep(n-1)]};
  for (const d of [0,1,2,3]) { const r=M.AnyComponentSchema.safeParse(deep(d));
    console.log(d, r.success, r.success?0:String(r.error.message).length); }
})"

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanefindingpackage: typespriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions