Skip to content

docs(blocks): teach a validation shape the input node actually declares (#5229) - #5251

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5229-forms-mdx-validation-dialect
Aug 18, 2026
Merged

docs(blocks): teach a validation shape the input node actually declares (#5229)#5251
os-support-ai merged 1 commit into
mainfrom
claude/issue-5229-forms-mdx-validation-dialect

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5229

Docs-only. One file: content/docs/blocks/forms.mdx.

The card's premise did not survive verification — the state is quieter, not louder

The card and the dispatch both hold that PR5230 (#5186) turned this example from quietly wrong into loudly wrong, and that objectui validate now "rejects this example by name". Measured on origin/main at 56735762f, it does not:

$ sed -n '43,52p' content/docs/blocks/forms.mdx > before.json
$ objectui validate before.json
✓ Schema is valid!
Type: input

The reason is a layer the card did not account for. The documented node is "type": "input"InputSchema, and InputSchema declares no validation key at all. BaseSchemaCore ends .passthrough(), so the whole validation object rides through unvalidated. FieldConstraintsSchema — the shape PR5230 hardened — is wired only into FormFieldSchema.validation (packages/types/src/zod/form.zod.ts:465), which belongs to a form node's fields[], not to an input node.

The same block on the face where that schema does apply is rejected, which confirms the hardening is real and simply out of reach here:

$ objectui validate probe-formfield.json # same validation block, inside {"type":"form","fields":[…]}
✗ Schema validation failed!
1. Invalid input Code: invalid_union

So the published example was never reached by the named rejection, and is not reached today.

What the defect actually is — worse than "a third dialect"

It is not a mis-spelling of a real key. It is an inert key on the wrong node type, invisible to every tool we ship:

SurfaceVerdict on the published example
objectui validateaccepts — validation is undeclared, BaseSchema is .passthrough()
InputRenderernever reads schema.validation; it reads schema.pattern (input.tsx:56)
form.tsx's #5099 diagnosticwalks a form node's fields[]; a standalone input node is not in it
check-doc-component-types.mjspasses — it judges the type literal only, deliberately
check-doc-snippet-types.mjsnever sees it — TS_FENCE_LANGUAGES is ts/tsx only; this file is all json

A reader copying it got zero validation and zero diagnostics. That is #5099's symptom, still fully alive on this surface and untouched by PR5230.

How far outside the validator the old key sat, measured:

$ echo '{"type":"input","name":"email","validation":{"pattern":12345,"message":false,"nonsense":[1,2]}}' | …
✓ Schema is valid! # complete garbage, accepted
$ echo '{"type":"input","name":"email","pattern":12345}' | …
✗ Schema validation failed! # the DECLARED key, wrong type — rejected

The teaching decision

The four blocks this page documents (forms/contact-form, settings-form, newsletter-signup, payment-form) are presentational trees of card/stack/label/inputzerovalidation keys, no "type": "form" node anywhere. Their inputs read exactly type/id/name/inputType/placeholder/required. So the honest route for this page is the input node's own declared keys, which InputRenderer forwards to the native HTML input attributes:

  • required, pattern (a stringInputSchema.pattern is z.string()), maxLength, min, max, step

That is not a fourth dialect: pattern is a first-class declared key on the exact node type the example uses, and it is the one the renderer actually reads.

The section then hands off to the form component for rule objects with custom messages, in the { value, message } shape FieldConstraintsSchema pins, and states why pattern alone cannot come from JSON there (react-hook-form runs it only when value instanceof RegExp) — pointing at the field-metadata route and the TypeScript form instead. Per the dispatch, no JSON snippet shows a pattern object implying it validates.

File sweep

Swept all 82 lines / 4 json fences. One occurrence of the flat dialect (the "Add Validation" block), now fixed. "Add Submit Action" and "Customize Layout" contain no validation, no flat pattern, and no stray sibling message.

Verification

Both published snippets validate, at 6297e3b3e:

$ objectui validate after-input.json → ✓ Schema is valid! Type: input
$ objectui validate after-form.json → ✓ Schema is valid! Type: form

Gate union re-run on the final commit 6297e3b3e, all green:

check-doc-component-types PASS 482 registered, 88 exempted — "Every documented component type is registered."
check-doc-links PASS "Links are valid across 13 scan roots."
check-control-bytes PASS scanned 4645 tracked text file(s)
check-changeset-presence PASS "1 file(s) changed, 0 of them under the src/ of a package the release covers … no changeset is owed."
check-doc-snippet-types PASS 67 of 67 block(s) judged, 0 failed (after a full packages build)
Build Docs PASS pnpm turbo run build --filter='@object-ui/site' → 29 successful, 29 total

check-changeset-presence.mjs was run rather than assumed: it reports no changeset owed, so none is added. The new type literals (form, textarea) are both registered, so the existing forms.mdx exemption in check-doc-component-types.mjs (submit) needed no amendment.

⚠️ Nothing we ship can check the corrected example

Stated plainly, as the card asked. check-doc-snippet-types.mjs compiles ts/tsx/typescript fences only — this file is entirely json, so its snippets never enter the compile set (corroborating #5174 from a different angle: the fence-language filter, not the .md/.mdx collection filter). check-doc-component-types.mjs judges the type literal only, by design. And objectui validate cannot discriminate here at all, because the keys in question are passthrough.

The corrected example is nevertheless a real improvement in checkability: it moves the declaration out of a key the validator cannot see and into pattern, which the validator does enforce (shown above). Filed as #5250 so the gap is tracked rather than implied — #5138 filed this class and was closed by PR #5161, which built the TypeScript half only.

Out of scope, filed not fixed

packages/types was not touched: per the dispatch it is the reference, and it is correct.


Generated by Claude Code

…es (#5229)
The "Add Validation" section of content/docs/blocks/forms.mdx taught a
`validation` object on a `"type": "input"` node, carrying a flat string
`pattern` and a sibling `message` key.
`InputSchema` declares no `validation` key at all, and `BaseSchema` is
`.passthrough()`, so the whole object rode through unvalidated: `objectui
validate` accepted it (it accepts `"validation": {"pattern": 12345,
"message": false}` just as happily), `InputRenderer` never reads
`schema.validation`, and form.tsx's #5099 diagnostic walks a form node's
`fields[]` rather than standalone input nodes. A reader copying the example
got zero validation and zero diagnostics from every tool we ship.
Replaced with the keys the node actually declares and the renderer actually
reads: `required` and `pattern` (a string), forwarded to the native HTML
input attributes at input.tsx:56. Added the `form` component's
`fields[].validation` route for rule objects with custom messages, in the
`{ value, message }` shape FieldConstraintsSchema pins, and stated why
`pattern` alone cannot come from JSON there — react-hook-form runs it only
when `value instanceof RegExp` — pointing at the field-metadata route and
the TypeScript form instead.
Swept the whole file: this was the only occurrence of the dialect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT. PM round 6, session session_01RV6yuVCxymHYE16PL9vQkE.

You falsified my premise, not the card's — and it was load-bearing

The urgency framing on #5229 was mine. I wrote it in the dispatch, in the card comment, in my ACCEPT on PR5230, and in a report to the maintainer: "PR5230 changed this example from quietly wrong to loudly wrong — objectui validate now rejects it by name."

You measured it accepted, and I re-verified every link before writing this rather than taking your report:

  • InputSchema = BaseSchema.extend({…})packages/types/src/zod/form.zod.ts:192
  • BaseSchema ends .passthrough()base.zod.ts:143, comment "Allow additional properties for type-specific extensions"
  • FieldConstraintsSchema has exactly one wiring site in the entire zod surface — form.zod.ts:465, inside FormFieldSchema, i.e. a form node's fields[]
  • InputRenderer reads schema.pattern (input.tsx:56), never schema.validation

So the hardened shape never reaches an input node, and my claim was false in both directions of time — the example was accepted before PR5230 and after.

The error was mine and specific: I read PR5230's changeset (correct: validate now rejects the flat dialect) and carried it to a documentation example without asking whether the documented node was one that schema governs. Between "the validator rejects this shape" and "the validator rejects this file" sits a node-type question I never asked. Corrected publicly on #5229.

Your counterfactual is what makes the diagnosis airtight rather than merely plausible: the samevalidation block inside {"type":"form","fields":[…]}is rejected (invalid_union). That distinguishes "the hardening is broken" from "the hardening is real and out of reach here" — and only the second is true.

The defect is worse than the card described, in the opposite direction

Not a mis-spelled key: an inert key on the wrong node type, invisible to every tool we ship. The measurement that makes it vivid:

{"type":"input","name":"email","validation":{"pattern":12345,"message":false,"nonsense":[1,2]}} -> ✓ Schema is valid!
{"type":"input","name":"email","pattern":12345} -> ✗ Schema validation failed!

Complete garbage accepted under the undeclared key; the declared key rejected for a type error. A reader copying the published example got zero validation and zero diagnostics#5099's symptom fully alive on this surface, untouched by PR5230. Quieter than I claimed, and quiet is worse than loud.

The fix is better than the one I specified

I told you to teach the field-metadata route on the JSON face. You taught the keys the input node actually declares and the renderer actually forwardsrequired, and pattern as a string, because InputSchema.pattern is z.string() and InputRenderer hands it to the native HTML attribute — then handed off to the form component for rule objects with custom messages, stating why pattern alone cannot come from JSON there.

That is strictly better on the axis that matters: it is not a fourth dialect (it is a first-class declared key on the exact node type the example uses), and it improves checkability — it moves the declaration out of a key the validator cannot see into one the validator does enforce, which you demonstrated rather than asserted. My version would not have done that.

The grounding in what the page actually documents is the right instinct too: all four blocks are presentational card/stack/label/input trees with zero validation keys and no form node anywhere, so teaching form-node syntax on this page would have been correct-but-unreachable advice.

Gates

18/18 check runs completed, zero failures. ACCEPT path surface: content/docs/blocks/forms.mdx, one file — no governed surface touched, probe run explicitly. check-changeset-presence.mjs was run rather than assumed and reports none owed. Whole-file sweep done as asked: 82 lines, 4 json fences, exactly one occurrence.

Flipping ready and enqueueing. #5229 closes on merge.

The gap you were asked to state plainly, and did

> Nothing we ship can check the corrected example.

check-doc-snippet-types compiles ts/tsx/typescript fences only and this file is entirely json; check-doc-component-types judges the type literal by design; objectui validate cannot discriminate because the keys are passthrough. Filed as #5250 so it is tracked rather than implied — and note it is a second, distinct angle on #5174: the fence-language filter, not the .md/.mdx collection filter. #5138 filed this class and PR5161 closed it having built the TypeScript half only.

#5249 is a genuine sibling: plugin-form.mdx:108 still types pattern.value as string | RegExp, contradicting the narrowed contract and its own Notes cell in the same row. Correctly filed to #5118's territory rather than folded in.


Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review August 18, 2026 20:26
@os-support-ai
os-support-ai added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit a40b571Aug 18, 2026
19 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5229-forms-mdx-validation-dialect branch August 18, 2026 20:26
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants

@os-support-ai@claude