derailed/k9s · error
expected TableRow, but got %T
Error message
expected TableRow, but got %T
What it means
The metav1.Table renderer requires the object to be a metav1.TableRow value type (not a pointer, not an unstructured). The assertion `o.(metav1.TableRow)` failed, so the pipeline delivered *unstructured.Unstructured, a typed object, or a *metav1.TableRow pointer. It indicates the wrong renderer is wired to the data source, or the row was double-wrapped.
Source
Thrown at internal/render/table.go:90
if c.Name == ageTableCol {
t.setAgeIndex(i)
continue
}
timeCol := ageCols.Has(c.Name)
h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name), Attrs: model1.Attrs{Time: timeCol, Wide: c.Priority > 0}})
}
if t.getAgeIndex() > 0 {
h = append(h, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}})
}
return h
}
// Render renders a K8s resource to screen.
func (t *Table) Render(o any, ns string, r *model1.Row) error {
row, ok := o.(metav1.TableRow)
if !ok {
return fmt.Errorf("expected TableRow, but got %T", o)
}
if err := t.defaultRow(&row, ns, r); err != nil {
return err
}
if t.specs.isEmpty() {
return nil
}
obj := row.Object.Object
if obj != nil {
obj = obj.DeepCopyObject()
}
cols, err := t.specs.realize(obj, t.defaultHeader(), r)
cols.hydrateRow(r)
return err
}
View on GitHub (pinned to 2d3ccc6ba2)
Solutions
- Pass the row by value: t.Render(row, ns, r) with row of type metav1.TableRow, dereferencing any pointer (*row).
- If data is unstructured, convert it to a TableRow with the right cells or use the unstructured-capable renderer (Service/Workload path).
- Check which renderer the view registered for the GVR and align the data source (Table vs informer) with it.
Example fix
// before err := tbl.Render(&row, ns, r) // after err := tbl.Render(row, ns, r)
Defensive patterns
Strategy: type-guard
Validate before calling
row, ok := o.(metav1.TableRow)
if !ok {
if ptr, isPtr := o.(*metav1.TableRow); isPtr {
row, ok = *ptr, true
}
}
if !ok { return fmt.Errorf("table renderer needs a TableRow, got %T", o) }
err := tbl.Render(row, ns, r) Type guard
func asTableRow(o any) (metav1.TableRow, bool) {
switch v := o.(type) {
case metav1.TableRow:
return v, true
case *metav1.TableRow:
return *v, true
default:
return metav1.TableRow{}, false
}
} Try / catch
if err := tbl.Render(obj, ns, r); err != nil && strings.Contains(err.Error(), "expected TableRow") {
slog.Error("table renderer got wrong type", "actualType", fmt.Sprintf("%T", obj))
} Prevention
- Pass metav1.TableRow by value, never a pointer.
- Keep one renderer per data source: Table renderer for metav1.Table pipelines, unstructured renderers for informer pipelines.
- Assert types in view tests with the same objects production feeds them.
When it happens
Trigger: Calling Table.Render(o, ns, r) where o is *unstructured.Unstructured (informal cache data), a typed pod/deployment struct, or *metav1.TableRow (pointer instead of value: %T prints *v1.TableRow). Also triggered when a custom viewer forwards the wrong element of a metav1.Table.Rows slice.
Common situations: Mixing informer-based browsing with table-based renderers; a decorator dereferencing level mismatch (passing &row instead of row); client-go upgrades where watch payloads change representation.
Related errors
- expected Unstructured, but got %T
- expected WorkloadRes but got %T
- not a metav1.Table
- expecting a ForwardRes but got %T
- expected Unstructured, but got %T
AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15).
Data as JSON: /api/errors/cbcd1be18b582cd7.
Report an issue: GitHub.