apache/beam · error

expected single value map, had

Error message

expected single value map, had %v - %v

What it means

getOnlyPair is a generic helper asserting a map has exactly one entry (used e.g. to fetch the single input/output of a transform). If the map has zero or multiple entries, prism panics with the length and full map contents because the expected structural invariant of the pipeline graph is violated.

Solutions

  1. Identify the transform from the map contents in the panic and check its expected input/output arity.
  2. Ensure transforms consuming this helper (like Combine) are not given additional side inputs.
  3. Inspect the expanded pipeline JSON to see why the map has multiple entries; restructure the pipeline accordingly.
  4. File a Beam issue if a standard transform triggers it — likely a graph-construction bug.

Example fix

// before
pc := beam.ParDo(s, fn, input, other) // adds extra input edge breaking single-input assumption
// after
res := beam.Combine(s, fn, input) // keep the combine single-input
Defensive patterns

Strategy: validation

Validate before calling

// Check map arity before calling getOnlyPair
if len(inputs) != 1 {
    return fmt.Errorf("expected single input, got %d", len(inputs))
}

Type guard

func isSingleEntry[K comparable, V any](m map[K]V) bool { return len(m) == 1 }

Try / catch

func safeGetOnlyPair[K comparable, V any](m map[K]V) (k K, v V, recovered any) {
    defer func() { recovered = recover() }()
    k, v = getOnlyPair(m)
    return
}

Prevention

When it happens

Trigger: getOnlyValue (or getOnlyPair) is called on a transform's inputs/outputs map that contains 0 or 2+ PCollection IDs — e.g. a Combine or other transform expected to be single-input/single-output but wired with extra side inputs or multiple outputs.

Common situations: Composite transforms producing unexpected arity, cross-language expansion yielding extra edges, or pipeline graph anomalies from optimizer/sibling-runner submissions.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/570666f925f46b05. Report an issue: GitHub.

Appendix: source

Thrown at sdks/go/pkg/beam/runners/prism/internal/execute.go:425

	c := coders[coldCId]
	if c.GetSpec().GetUrn() == urns.CoderKV {
		return c.GetComponentCoderIds()[0], true
	}
	return "", false
}

func getWindowValueCoders(comps *pipepb.Components, col *pipepb.PCollection, coders map[string]*pipepb.Coder) (engine.WinCoderType, exec.WindowDecoder, exec.WindowEncoder) {
	ws := comps.GetWindowingStrategies()[col.GetWindowingStrategyId()]
	wcID, err := lpUnknownCoders(ws.GetWindowCoderId(), coders, comps.GetCoders())
	if err != nil {
		panic(err)
	}
	return makeWindowCoders(coders[wcID])
}

func getOnlyPair[K comparable, V any](in map[K]V) (K, V) {
	if len(in) != 1 {
		panic(fmt.Sprintf("expected single value map, had %v - %v", len(in), in))
	}
	for k, v := range in {
		return k, v
	}
	panic("unreachable")
}

func getOnlyValue[K comparable, V any](in map[K]V) V {
	_, v := getOnlyPair(in)
	return v
}

// buildTrigger converts the protocol buffer representation of a trigger
// to the engine representation.
func buildTrigger(tpb *pipepb.Trigger) engine.Trigger {
	switch at := tpb.GetTrigger().(type) {
	case *pipepb.Trigger_AfterAll_:
		subTriggers := make([]engine.Trigger, 0, len(at.AfterAll.GetSubtriggers()))

View on GitHub (pinned to 12126d8942)