Skip to content

The designer's object payload sends fields as an ARRAY where ObjectSchema requires a map — a value-level rejection the key-name parity gate cannot see #6240

Description

@yinlianghui

Found while fixing objectui#6223 (the object-level members of the #5761 parity family). Filed, not fixed: #6223 closes the key-name class, and this is a value-level rejection — the gate's own coverage note 4, explicitly outside what a key-name parity check can reach.

The measurement

Measured against the installed @objectstack/spec 17.2.0, ESM build (dist/data/index.mjs), on origin/main @ 0409b766d:

ObjectSchema.safeParse({ name: 'account', label: 'Account' })
=> success = false invalid_type @ fields // `fields` is REQUIRED
ObjectSchema.safeParse({ name: 'account', label: 'Account',
fields: [ { name: 'n', type: 'text', label: 'N' } ] })
=> success = false invalid_type @ fields // an ARRAY is refused
ObjectSchema.safeParse({ name: 'account', label: 'Account',
fields: { n: { type: 'text', label: 'N' } } })
=> success = true // a MAP is what it wants

So fields is required and must be keyed by field name.

The writers

packages/app-shell/src/services/MetadataService.ts — both object writers emit an array:

  • saveObject(obj, existingFields)toObjectPayload sets fields from FieldMetadataPayload[], and the declared shape is fields?: FieldMetadataPayload[].
  • saveFields(objectName, fields)fields: fields.map(toFieldPayload), spread over the object fetched from the server. Note what that spread means: the server's own document arrives with fields as a map and this line replaces it with an array, so the shape is converted in the wrong direction on every field save.

By contrast packages/plugin-designer/src/MetadataFieldsPage.tsx writes fields as a map (Record<string, unknown>), which is the correct shape. The two designer write paths disagree with each other, and only one of them agrees with the spec.

Verified as an executable assertion in packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts (landed by #6223), which pins the remaining failure exactly so it cannot silently change:

expect(unrecognizedKeys(result)).toEqual([]);// key names: clean after #6223expect(result.error?.issues.map((i)=>`${i.code} @ ${i.path.join('.')}`)).toEqual(['invalid_type @ fields']);// value level: this card

Why it is not a one-liner

  • saveObject's existingFields parameter is typed and documented as an array and is public API (MetadataService is exported from app-shell's barrel via useMetadataService), so changing the payload shape and changing the method signature are two separable decisions.
  • The map's KEY has to come from somewhere. FieldMetadataPayload.name is the obvious candidate, but it is optional on some read paths, and a field with no name would silently vanish into a { undefined: … } entry rather than failing loudly — which is the AI-authored-metadata failure mode this repo keeps closing, not opening.
  • saveFields currently preserves unknown server keys by spreading the fetched document. A map/array conversion has to preserve that property or it becomes a data-loss bug on save.
  • Whether the route is lenient about this today is unmeasured. This issue reports the schema fact, not a reproduced HTTP response.

Refs: #6223 (the key-name half + the object oracle) · #5761 (the gate, coverage note 4) · #6238

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions