derailed/k9s · error
expecting *Unstructured but got %T
Error message
expecting *Unstructured but got %T
What it means
After locating pods by service selector, the xray Service renderer (internal/xray/svc.go:52) asserts each returned runtime.Object to *unstructured.Unstructured before wrapping it in render.PodWithMetrics. The informer/lister backing the pod lookup is expected to serve unstructured objects; if it returns typed objects (or any other implementation), rendering aborts.
Source
Thrown at internal/xray/svc.go:52
return err
}
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
root := NewTreeNode(client.SvcGVR, client.FQN(svc.Namespace, svc.Name))
oo, err := s.locatePods(ctx, svc.Namespace, svc.Spec.Selector)
if err != nil {
return err
}
ctx = context.WithValue(ctx, KeyParent, root)
var re Pod
for _, o := range oo {
p, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expecting *Unstructured but got %T", o)
}
if err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {
return err
}
}
root.Extras[StatusKey] = OkStatus
if root.IsLeaf() {
return nil
}
gvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, svc.Namespace)
nsn := parent.Find(gvr, nsID)
if nsn == nil {
nsn = NewTreeNode(gvr, nsID)
parent.Add(nsn)
}
nsn.Add(root)
View on GitHub (pinned to 2d3ccc6ba2)
Solutions
- Serve pods from an unstructured informer (dynamic client factory) as the standard k9s watch factory does
- In tests, return &unstructured.Unstructured{...} from locatePods stubs instead of *corev1.Pod
- Convert before rendering when a typed object is unavoidable: runtime.DefaultUnstructuredConverter.ToUnstructured(typedObj) then wrap
- Check the %T output to identify which informer produced the typed object
Example fix
// before: typed pod from a typed informer
var o runtime.Object = typedPod // *corev1.Pod
p, ok := o.(*unstructured.Unstructured) // !ok -> error
// after: convert
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(typedPod)
if err != nil { return err }
p := &unstructured.Unstructured{Object: u} Defensive patterns
Strategy: type-guard
Validate before calling
// verify informer output type before the render loop
for _, o := range oo {
if _, ok := o.(*unstructured.Unstructured); !ok {
return fmt.Errorf("informer returned %T; unstructured required", o)
}
} Type guard
func asUnstructured(o runtime.Object) (*unstructured.Unstructured, bool) {
u, ok := o.(*unstructured.Unstructured)
return u, ok
} Try / catch
// comma-ok with conversion fallback for typed objects
u, ok := o.(*unstructured.Unstructured)
if !ok {
m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
if err != nil { return err }
u = &unstructured.Unstructured{Object: m}
} Prevention
- Back pod lookups with dynamic/unstructured informers, not typed client-go informers
- In unit tests, stub listers to return &unstructured.Unstructured{Object: map[string]interface{}{...}}
- Assert object types at the DAO boundary so renderers can trust their inputs
When it happens
Trigger: A factory/informer cache configured with typed client-go informers for pods while xray expects unstructured ones; test doubles injecting fake runtime.Objects that are not *unstructured.Unstructured; forks mixing typed and unstructured DAOs.
Common situations: Custom k9s builds swapping informer factories; upgrading client-go where lister behavior changed; unit tests with generated mocks failing the assertion.
Related errors
- expecting a TreeNode but got %T
- expecting a factory but got %T
- invalid command. use `xray xxx`
- expecting a switchable resource
- expecting a switchable resource
AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15).
Data as JSON: /api/errors/841a86e44324cbde.
Report an issue: GitHub.