Found while implementing #3910 (converting the docs field catalog to form hosting). Out of that card's scope — filed rather than fixed, because picking the surviving spelling is a contract decision with a live blast radius.
The divergence
A grid field's columns are declared by GridColumnDefinition in packages/types/src/field-types.ts:677, which is also the declared type of GridFieldMetadata.columns (field-types.ts:651):
export interface GridColumnDefinition {
/** Column field name */
name: string;
label?: string;
type: string;
// ...
}
But the widget that renders those columns, packages/fields/src/widgets/GridField.tsx:41, declares its own local interface and reads a different key:
export interface GridColumn {
field: string;
// ...
}
Every read in GridField is c.field — key={c.field} (:558, :590, :620), blank[c.field] (:399), applyCell(rowIdx, col.field, …) (:449), columns.findIndex((c) => c.field === totalField) (:528), extraShown.has(c.field) (:361). There is no normalization from name.
Consequence
Metadata authored against the declared type renders a grid whose cells are all empty, plus a React key warning (because key={c.field} is undefined for every column).
Measured by rendering GridField directly — no form, no docs wrapper, identical rows, only the column key spelling differs:
name-spelled (GridColumnDefinition / catalog): key-warnings=1 rows=[["1","IN[]","IN[]","IN[]"],["2","IN[]","IN[]","IN[]"],["3","IN[]","IN[]","IN[]"]]
field-spelled (GridField GridColumn) : key-warnings=0 rows=[["1","IN[Widget A]","IN[2]","IN[29.99]"],["2","IN[Widget B]","IN[1]","IN[49.99]"],["3","IN[]","IN[]","IN[]"]]
(IN[x] = the cell input's value; row 3 is the ghost/new row.)
Which side each producer is on:
name — GridColumnDefinition (the declared type), the three fields-grid catalog examples, and the prose on content/docs/fields/grid.mdx ("name: string; // Column field name").
field — GridField itself, and the master-detail derivation in packages/plugin-form/src/deriveMasterDetail.ts (hydrateColumns reads fields[col.field] at :261).
So real app paths that go through deriveMasterDetail work, and anything authored from the published type or the documentation does not. That is the worst direction for this repo's second axis: an AI author reading GridColumnDefinition or the grid docs page produces a grid that silently renders nothing in every cell.
Visible today
The three demos on /docs/fields/grid render tables with the correct row COUNT and every cell empty (browser-verified during #3910). This predates #3910 — the old docs-demo wrapper passed the same name-spelled columns through, so the cells were empty there too. #3910 deliberately did not re-spell the examples to field: that would fossilize into the documentation a key the declared type does not have, which is the exact failure class #3798 ruling B set out to remove.
Two resolutions, and why this needs a maintainer
- Make
GridField read the declared name (contract-first, AGENTS.md #0.1 — the consumer is the divergent side). Cost: must migrate deriveMasterDetail/hydrateColumns and any app metadata already passing field, or accept both during a deprecation window, which is itself the tolerant-alias smell.
- Change the declared type to
field and fix GridColumnDefinition, the catalog examples and the grid docs prose. Cost: breaks any metadata authored against the currently published type.
Option 1 matches the stated contract and the documentation; option 2 matches what the live code actually does. Either way the fix must land on ONE side — a renderer-side col.field ?? col.name alias is the thing AGENTS.md #0.1 forbids, and would leave two dialects in the catalog forever.
Related family (types-vs-spec drift, not the same key): #2231, #2890.
Reproduce
pnpm exec vitest run packages/fields/src/widgets/ # existing tests all pass — nothing covers this
Render GridField with value set to two row objects and columns spelled name, and observe empty cells plus the key warning; re-spell to field and both go away.
Found while implementing #3910 (converting the docs field catalog to form hosting). Out of that card's scope — filed rather than fixed, because picking the surviving spelling is a contract decision with a live blast radius.
The divergence
A grid field's
columnsare declared byGridColumnDefinitioninpackages/types/src/field-types.ts:677, which is also the declared type ofGridFieldMetadata.columns(field-types.ts:651):But the widget that renders those columns,
packages/fields/src/widgets/GridField.tsx:41, declares its own local interface and reads a different key:Every read in
GridFieldisc.field—key={c.field}(:558,:590,:620),blank[c.field](:399),applyCell(rowIdx, col.field, …)(:449),columns.findIndex((c) => c.field === totalField)(:528),extraShown.has(c.field)(:361). There is no normalization fromname.Consequence
Metadata authored against the declared type renders a grid whose cells are all empty, plus a React key warning (because
key={c.field}isundefinedfor every column).Measured by rendering
GridFielddirectly — no form, no docs wrapper, identical rows, only the column key spelling differs:(
IN[x]= the cell input's value; row 3 is the ghost/new row.)Which side each producer is on:
name—GridColumnDefinition(the declared type), the threefields-gridcatalog examples, and the prose oncontent/docs/fields/grid.mdx("name: string; // Column field name").field—GridFielditself, and the master-detail derivation inpackages/plugin-form/src/deriveMasterDetail.ts(hydrateColumnsreadsfields[col.field]at:261).So real app paths that go through
deriveMasterDetailwork, and anything authored from the published type or the documentation does not. That is the worst direction for this repo's second axis: an AI author readingGridColumnDefinitionor the grid docs page produces a grid that silently renders nothing in every cell.Visible today
The three demos on
/docs/fields/gridrender tables with the correct row COUNT and every cell empty (browser-verified during #3910). This predates #3910 — the old docs-demo wrapper passed the samename-spelledcolumnsthrough, so the cells were empty there too. #3910 deliberately did not re-spell the examples tofield: that would fossilize into the documentation a key the declared type does not have, which is the exact failure class #3798 ruling B set out to remove.Two resolutions, and why this needs a maintainer
GridFieldread the declaredname(contract-first, AGENTS.md #0.1 — the consumer is the divergent side). Cost: must migratederiveMasterDetail/hydrateColumnsand any app metadata already passingfield, or accept both during a deprecation window, which is itself the tolerant-alias smell.fieldand fixGridColumnDefinition, the catalog examples and the grid docs prose. Cost: breaks any metadata authored against the currently published type.Option 1 matches the stated contract and the documentation; option 2 matches what the live code actually does. Either way the fix must land on ONE side — a renderer-side
col.field ?? col.namealias is the thing AGENTS.md #0.1 forbids, and would leave two dialects in the catalog forever.Related family (types-vs-spec drift, not the same key): #2231, #2890.
Reproduce
Render
GridFieldwithvalueset to two row objects andcolumnsspelledname, and observe empty cells plus the key warning; re-spell tofieldand both go away.