Skip to content

finding(types): the zod layout mirror substitutes runtime .default() values the renderers never apply — a parsed container renders a different width than an unparsed one #7735

Description

@os-justin

Found while measuring objectui#7361's triage boundary 3 (does anything downstream consume the declared default). Unassigned. Higher stakes than objectui#7361, because this surface is RUNTIME rather than documentation.

Measured on origin/main = b74a859, against the built packages/types/dist.

What is declared

packages/types/src/zod/layout.zod.ts is a published runtime validator (@object-ui/types/zod, reached through safeValidateSchema, which is AnyComponentSchema.safeParse and returns the parsed object as result.data). Several members carry .optional().default(VALUE), and .default() is not documentation — parse SUBSTITUTES that value into the output when the key is absent.

  layout.zod.ts:170   maxWidth: ... .optional().default('lg')
  layout.zod.ts:191   align:    ... .optional().default('center')    (on FlexSchema only)

What the renderers apply

  container.tsx:26   const maxWidth = schema.maxWidth ?? 'xl';
  flex.tsx:18        const align = schema.align || 'start';
  stack.tsx:24       const align = schema.align || 'stretch';

Measured, not inferred

Feeding three minimal nodes through the built safeValidateSchema:

container   success: true | parsed: {"type":"container","maxWidth":"lg","centered":true}
flex        success: true | parsed: {"type":"flex","direction":"row","justify":"start","align":"center","gap":2,"wrap":false}
stack       success: true | parsed: {"type":"stack"}

The consequence

A container that omits maxWidth renders max-w-xl when it reaches the renderer as authored, and max-w-lg when it has been through the mirror first — the mirror writes 'lg' into the node, so the renderer's ?? finds a value and never applies its own default. Same shape for flex and align: 'center' is substituted, and 'center' is a value NEITHER flex nor stack applies. One authored document, two code paths, two layouts.

StackSchema in the same file declares no defaults at all (see the stack row above), so it is unaffected by the substitution — the mirror is also internally inconsistent between the two sibling types.

Relationship to other cards

  • objectui#7361 corrected the JSDoc @default on ContainerSchema.maxWidth and FlexLayoutProps.align. Its file surface was the docblocks only and its dispatch explicitly ruled out moving any default VALUE, so the mirror was deliberately left as it stands. After that change the two surfaces disagree with each other in the tree, which is part of why this needs its own ruling.
  • Distinct from objectui#5927, which is about mirrors being NARROWER than their TypeScript declarations (accept-set drift). This is default substitution — a different axis.
  • Another instance of objectui#4631.

Why a ruling rather than a patch

Two candidate shapes, and an implementing agent should not pick between them:

  1. Move the values (.default('lg') becomes .default('xl'), .default('center') becomes .default('start')). This changes what parse emits for every existing document, so it is a behaviour change on a published surface.
  2. Drop .default() from the mirror entirely, so parsing stops substituting and the renderer's fallback is the single source of the default. This is arguably the contract-first shape, and it is already what StackSchema does.

The wider question behind both: whether a validator on this surface should be allowed to author values at all.

Filed by an ObjectUI development agent while working objectui#7361. No assignee, no labels — left for triage.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions