Surfaced by the key-level parity gate built for #5761 (scripts/check-designer-field-key-parity.mjs). Filed rather than fixed: #5761's ruling is that the gate surfaces instances and each surfaced key gets its own card, because the three prior instances (#4644indexed, #4687distance_metric, #4676placeholder) took three different correct resolutions.
The key
referenceTo is not in FieldSchema's accept set. Measured against the installed @objectstack/spec 17.2.0:
FieldSchema.safeParse({ type:'lookup', label:'Account', referenceTo:'account' })
=> success = false
=> [{ code: 'unrecognized_keys', keys: ['referenceTo'] }]
The spec's spelling is reference:
reference — Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects.
Why this is live, not latent
Unlike sortOrder (declared but never populated), referenceTo has a live form control and two live writers:
packages/plugin-designer/src/FieldDesigner.tsx:311 — the field editor renders an input for it, visibleWhen: "record.type == 'lookup'".packages/plugin-designer/src/FieldDesigner.tsx:221,242 — both the create and the update path write referenceTo: data.referenceTo ? String(data.referenceTo) : undefined.packages/plugin-designer/src/MetadataFieldsPage.tsx — fromDesignerField writes referenceTo: designed.referenceTo onto the ServerFieldSchema that is PUT.packages/app-shell/src/services/MetadataService.ts:104 — toFieldPayload writes referenceTo: field.referenceTo onto FieldMetadataPayload, which saveFields PUTs as fields.map(toFieldPayload).
So an author who creates a lookup field in the designer and fills in the referenced object writes a key the API refuses. This is the #4644 shape exactly: PUT /api/v1/meta/object/:name returns a hard 422 INVALID_METADATA, which then blocks every subsequent save of that object until the key is cleared — and the author has no way to tell from the designer UI which key did it.
Also note the read direction is broken symmetrically: toDesignerField reads raw.referenceTo from the server payload, but a spec-parsed server sends reference, so an existing lookup field loads into the designer with an empty reference box.
Why this is not a mechanical rename in this card
reference is the obvious candidate, but choosing it is an adjudication:
Both directions (read and write), both designers, and the i18n label key appDesigner.fieldDesigner.referenceTo are in scope for whoever takes it.
Verification
pnpm check:designer-field-key-parity reports it. The key is recorded in that gate's KNOWN_UNPARSEABLE_KEYS ledger pointing here; removing it from the payload shapes without removing the ledger entry is also red, so the ledger cannot go stale.
Refs: #5761 (the gate) · #4644 · #4687 · #4676
Surfaced by the key-level parity gate built for #5761 (
scripts/check-designer-field-key-parity.mjs). Filed rather than fixed: #5761's ruling is that the gate surfaces instances and each surfaced key gets its own card, because the three prior instances (#4644indexed, #4687distance_metric, #4676placeholder) took three different correct resolutions.The key
referenceTois not inFieldSchema's accept set. Measured against the installed@objectstack/spec17.2.0:The spec's spelling is
reference:Why this is live, not latent
Unlike
sortOrder(declared but never populated),referenceTohas a live form control and two live writers:packages/plugin-designer/src/FieldDesigner.tsx:311— the field editor renders an input for it,visibleWhen: "record.type == 'lookup'".packages/plugin-designer/src/FieldDesigner.tsx:221,242— both the create and the update path writereferenceTo: data.referenceTo ? String(data.referenceTo) : undefined.packages/plugin-designer/src/MetadataFieldsPage.tsx—fromDesignerFieldwritesreferenceTo: designed.referenceToonto theServerFieldSchemathat is PUT.packages/app-shell/src/services/MetadataService.ts:104—toFieldPayloadwritesreferenceTo: field.referenceToontoFieldMetadataPayload, whichsaveFieldsPUTs asfields.map(toFieldPayload).So an author who creates a lookup field in the designer and fills in the referenced object writes a key the API refuses. This is the #4644 shape exactly:
PUT /api/v1/meta/object/:namereturns a hard 422INVALID_METADATA, which then blocks every subsequent save of that object until the key is cleared — and the author has no way to tell from the designer UI which key did it.Also note the read direction is broken symmetrically:
toDesignerFieldreadsraw.referenceTofrom the server payload, but a spec-parsed server sendsreference, so an existing lookup field loads into the designer with an empty reference box.Why this is not a mechanical rename in this card
referenceis the obvious candidate, but choosing it is an adjudication:FieldSchemamarksreferencerequired for relationship types, so a rename changes what a half-filled designer draft parses as — today the empty case isundefinedand the field saves.indexedanddistance_metric#4687 shows "delete the declaration" is sometimes the right answer even when a near-spelling exists.placeholder, a key FieldSchema rejects by name — but the designer's own preview renders it, so this is a contract question not a deletion #4676 shows "move the producer upstream" is sometimes the right answer.Both directions (read and write), both designers, and the i18n label key
appDesigner.fieldDesigner.referenceToare in scope for whoever takes it.Verification
pnpm check:designer-field-key-parityreports it. The key is recorded in that gate'sKNOWN_UNPARSEABLE_KEYSledger pointing here; removing it from the payload shapes without removing the ledger entry is also red, so the ledger cannot go stale.Refs: #5761 (the gate) · #4644 · #4687 · #4676