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

  1. Pass the row by value: t.Render(row, ns, r) with row of type metav1.TableRow, dereferencing any pointer (*row).
  2. If data is unstructured, convert it to a TableRow with the right cells or use the unstructured-capable renderer (Service/Workload path).
  3. 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

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


AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15). Data as JSON: /api/errors/cbcd1be18b582cd7. Report an issue: GitHub.