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 cardWhy 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
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/spec17.2.0, ESM build (dist/data/index.mjs), onorigin/main@0409b766d:So
fieldsis 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)→toObjectPayloadsetsfieldsfromFieldMetadataPayload[], and the declared shape isfields?: 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 withfieldsas 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.tsxwritesfieldsas 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:Why it is not a one-liner
saveObject'sexistingFieldsparameter is typed and documented as an array and is public API (MetadataServiceis exported from app-shell's barrel viauseMetadataService), so changing the payload shape and changing the method signature are two separable decisions.FieldMetadataPayload.nameis 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.saveFieldscurrently 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.Refs: #6223 (the key-name half + the object oracle) · #5761 (the gate, coverage note 4) · #6238