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

  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.