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.

Solutions

  1. Serve pods from an unstructured informer (dynamic client factory) as the standard k9s watch factory does
  2. In tests, return &unstructured.Unstructured{...} from locatePods stubs instead of *corev1.Pod
  3. Convert before rendering when a typed object is unavoidable: runtime.DefaultUnstructuredConverter.ToUnstructured(typedObj) then wrap
  4. 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

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


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

Appendix: 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)