Skip to content

[P3] spec: z.input of every z.ZodType<T>-annotated recursive schema is unknown, so the authoring surface under it is unchecked #4195

Description

@os-zhuang

Found while fixing #4171 (PR #4194). Out of scope there; filing rather than widening it.

The mechanism

Zod 4's ZodType is ZodType<out Output = unknown, out Input = unknown, …>. Every recursive schema in the spec is annotated with one type argument:

exportconstNavigationItemSchema: z.ZodType<NavigationItem>=z.lazy(()=>);// ^ Output only — Input stays `unknown`

So after #4171z.infer is precise, but z.input is unknown. That propagates to every schema that embeds one:

typeNavInput=z.input<typeofAppSchema>['navigation'];// unknown[] | undefinedtypeFieldInput=z.input<typeofFormFieldSchema>;// unknown

defineApp(config: z.input<typeof AppSchema>) therefore accepts any array of anything for navigation. Same for FormField under FormSection.fields, and for QueryInput['joins'] / ['fields'].

This predates #4171z.ZodType<any> also left Input at unknown — so nothing regressed. It is the other half of the same defect: #4171 fixed what a consumer reads, this is what an author writes.

Affected: NavigationItemSchema, FormFieldSchema, FilterConditionSchema, QuerySchema, StateNodeSchema, ValidationRuleSchema, JoinNodeSchema, NormalizedFilterSchema, FieldNodeSchema — every z.ZodType<T> annotation in packages/spec/src.

Why it's worth an issue rather than a note

It has already cost something concrete. #4171 made NavigationItem precise, which correctly flagged that SETUP_APP: App / STUDIO_APP: App / SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] are authoring literals annotated with the parsed type, where .default()ed keys (expanded, target) are required. The sanctioned fix for that shape is "type them as the input surface" (the z.input fix #4074 applied in types, cited in objectui's check-type-check-coverage.mjs) — but retyping them to AppInput today would swap eight loud errors for no checking at all, because AppInput['navigation'] is unknown[].

So PR #4194 took the other route and spelled the defaults out in the metadata. That is honest and matches what those literals already do for active / isDefault / collapsible / collapsed / columns — but it is defaults-in-metadata, and it only exists because the input type is unusable.

unknown is not as dangerous as any (it forces narrowing on read rather than silently permitting member access), which is why this is P3 and not P2. But for a metadata-driven platform the authorable surface is the third-party API, and right now nothing type-checks it for these nodes.

The fix

Supply both parameters, deriving the input type the same way #4194 derives the output one:

exporttypeNavigationItemInput=|(z.input<typeofObjectNavItemSchema>&{children?: NavigationItemInput[]})|z.input<typeofDashboardNavItemSchema>||(z.input<typeofGroupNavItemSchema>&{children: NavigationItemInput[]});exportconstNavigationItemSchema: z.ZodType<NavigationItem,NavigationItemInput>=z.lazy(()=>);

QueryAST / QueryInput in data/query.zod.ts already keep exactly this pair by hand; this makes the schema annotation carry it.

Expect fallout, which is the point: every defineApp / defineView call site and every *Input-typed artifact gets type-checked against the nav / form-field / join vocabulary for the first time. Worth doing deliberately rather than as a side effect of #4171. When it lands, the explicit expanded: false / target: '_self' / span: 'auto' added by #4194 can be deleted in favour of retyping those three artifacts to the input surface.

The check

check:exported-any (added by #4194) reads the output side only. Extending it to fail on an exported schema whose input is unknown while its output is precise would ratchet this the same way — the asymmetry is the tell, and it is mechanically detectable from the built .d.ts.

Refs #4171, #4115, #4074, objectui#3042.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions