Filed by the #4581 seat (PR #4603) while measuring that PR's published .d.ts. Not fixed there — widening a published runtime validator is a contract decision of its own, and #4581's card scoped to the TS declarations.
The drift
packages/types/src/zod/base.zod.ts is a runtime validator mirroring BaseSchema. Three of its keys are now NARROWER than the TypeScript declaration they mirror:
| Key |
BaseSchema (TS, src/base.ts) |
BaseSchemaCore (zod, src/zod/base.zod.ts) |
Widened by |
visible |
boolean | string (:119) |
z.boolean().optional() (:100) |
PR #4593 (#4581) |
disabled |
boolean | string |
z.boolean().optional() (:127) |
PR #4603 (#4581) |
ariaLabel |
string | KeyedI18nLabel |
z.string().optional() (:142) |
PR #4603 (#4581) |
So an author can now write disabled: "${data.status === 'locked'}" — a capability the renderer implements (SchemaRenderer.tsx:466 evaluates it through evaluateCondition) and the type declares — and BaseSchemaCore.parse() will reject it. .passthrough() does not rescue any of the three: it admits UNDECLARED keys, and all three are explicitly declared, so the declared narrow validator wins.
Why this is observation-class rather than a live defect
Measured, nothing enforces it on the render path today:
packages/core/src/validation/schema-validator.ts — the validateSchema that SchemaRenderer calls — imports only the TS type BaseSchema (:19). It is hand-written and never touches the zod schema.
SchemaRenderer.tsx:502 computes _validation under __DEV__ only, and the sole consequence is a data-obj-schema-invalid attribute (:631). No hard reject.
- The zod
BaseSchema value is consumed only by its siblings inside packages/types/src/zod/** (app, blocks, theme, form, navigation, reports, feedback, views, … all .extend(...) it).
So no user hits this today. It matters because @object-ui/types/zod is a PUBLISHED validator surface: any consumer that parses an authored schema with it gets a rejection for a spelling the published types invite. That is the "declared = enforced" property inverted — the type says yes, the validator says no.
Also worth a decision, not a guess
Widening the zod side is not mechanical, which is exactly why this is a card and not a drive-by:
There is a second, larger question underneath: whether this hand-written zod mirror should track BaseSchema by construction (derivation or a parity test) rather than by hand, so the next widening cannot silently drift again. #2231 asks the adjacent question for ListViewSchema against @objectstack/spec/ui; this one is BaseSchema against its OWN TypeScript declaration, so it is filed standalone rather than as a sub-issue.
Refs #4581, PR #4593, PR #4603, #4580 (comment 5284007579), #4167.
Generated by Claude Code
Filed by the #4581 seat (PR #4603) while measuring that PR's published
.d.ts. Not fixed there — widening a published runtime validator is a contract decision of its own, and #4581's card scoped to the TS declarations.The drift
packages/types/src/zod/base.zod.tsis a runtime validator mirroringBaseSchema. Three of its keys are now NARROWER than the TypeScript declaration they mirror:BaseSchema(TS,src/base.ts)BaseSchemaCore(zod,src/zod/base.zod.ts)visibleboolean | string(:119)z.boolean().optional()(:100)disabledboolean | stringz.boolean().optional()(:127)ariaLabelstring | KeyedI18nLabelz.string().optional()(:142)So an author can now write
disabled: "${data.status === 'locked'}"— a capability the renderer implements (SchemaRenderer.tsx:466evaluates it throughevaluateCondition) and the type declares — andBaseSchemaCore.parse()will reject it..passthrough()does not rescue any of the three: it admits UNDECLARED keys, and all three are explicitly declared, so the declared narrow validator wins.Why this is observation-class rather than a live defect
Measured, nothing enforces it on the render path today:
packages/core/src/validation/schema-validator.ts— thevalidateSchemathatSchemaRenderercalls — imports only the TS typeBaseSchema(:19). It is hand-written and never touches the zod schema.SchemaRenderer.tsx:502computes_validationunder__DEV__only, and the sole consequence is adata-obj-schema-invalidattribute (:631). No hard reject.BaseSchemavalue is consumed only by its siblings insidepackages/types/src/zod/**(app,blocks,theme,form,navigation,reports,feedback,views, … all.extend(...)it).So no user hits this today. It matters because
@object-ui/types/zodis a PUBLISHED validator surface: any consumer that parses an authored schema with it gets a rejection for a spelling the published types invite. That is the "declared = enforced" property inverted — the type says yes, the validator says no.Also worth a decision, not a guess
Widening the zod side is not mechanical, which is exactly why this is a card and not a drive-by:
visible/disabled->z.union([z.boolean(), z.string()])is straightforward.ariaLabel-> the KEYED object ({ key, defaultValue?, params? }, namedKeyedI18nLabelin@object-ui/typessince fix(types): ariaLabel declares the keyed vocabulary, disabled accepts the predicate string (#4581) #4603) is a new object shape in the validator, and it must NOT be confused with the spec's inline locale map — the objectui#4167 hazard that finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580's Q2-B ruling turned on. Whoever takes this should read that ruling first.There is a second, larger question underneath: whether this hand-written zod mirror should track
BaseSchemaby construction (derivation or a parity test) rather than by hand, so the next widening cannot silently drift again. #2231 asks the adjacent question forListViewSchemaagainst@objectstack/spec/ui; this one isBaseSchemaagainst its OWN TypeScript declaration, so it is filed standalone rather than as a sub-issue.Refs #4581, PR #4593, PR #4603, #4580 (comment 5284007579), #4167.
Generated by Claude Code