alibaba/spring-ai-alibaba · error · IllegalArgumentException

GraphFlux done result map keys must be String

Error message

GraphFlux done result map keys must be String

What it means

NodeExecutor.copyStateMap copies the map supplied to GraphFlux/Data.done() into the typed graph state Map<String,Object>. Graph state keys must be Strings; if any key is a non-String (e.g. Integer, enum, raw key from another map), IllegalArgumentException 'GraphFlux done result map keys must be String' is thrown.

Source

Thrown at spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/executor/NodeExecutor.java:811

					return copyStateMap(resultMap);
				}
			}
		}

		Map<String, Object> state = new HashMap<>();
		state.put(graphFluxStateKey(graphFlux), lastData);
		return state;
	}

	private static String graphFluxStateKey(GraphFlux<?> graphFlux) {
		return StringUtils.hasText(graphFlux.getKey()) ? graphFlux.getKey() : "result";
	}

	private static Map<String, Object> copyStateMap(Map<?, ?> resultMap) {
		Map<String, Object> state = new HashMap<>();
		for (Map.Entry<?, ?> entry : resultMap.entrySet()) {
			if (!(entry.getKey() instanceof String key)) {
				throw new IllegalArgumentException("GraphFlux done result map keys must be String");
			}
			state.put(key, entry.getValue());
		}
		return state;
	}

	/**
	 * Checks interruptAfter hook for streaming nodes using the pre-merge state.
	 * <p>
	 * This method must be called <strong>before</strong> the streaming state updates are
	 * merged into the {@link OverAllState} to keep semantics consistent with the
	 * non-streaming interruptAfter hook.
	 * @param context the graph runner context
	 * @param actionResult the streaming node action result (state delta) passed to interruptAfter
	 * @return interruption metadata if the hook triggers
	 */
	private Optional<InterruptionMetadata> interruptAfterForStreaming(GraphRunnerContext context,
			Map<String, Object> actionResult) {

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Convert all keys to String before calling done: map.entrySet() -> Map.of(String.valueOf(k), v).
  2. If keys are enums or IDs, map them to their configured state property names (String) first.
  3. Add a pre-check validating the map is Map<String, ?> before passing it to Data.done().

Example fix

// before
Map<Integer, Object> byId = ...;
return Data.done(byId);
// after
Map<String, Object> state = byId.entrySet().stream()
    .collect(Collectors.toMap(e -> String.valueOf(e.getKey()), Map.Entry::getValue));
return Data.done(state);
Defensive patterns

Strategy: type-guard

Validate before calling

for (Map.Entry<?, ?> e : doneMap.entrySet()) {
    if (!(e.getKey() instanceof String)) {
        throw new IllegalStateException("done map key not a String: " + e.getKey());
    }
}

Type guard

boolean hasStringKeys(Map<?, ?> m) {
    return m.keySet().stream().allMatch(String.class::isInstance);
}

Try / catch

try {
    state = copyStateMap(resultMap);
} catch (IllegalArgumentException e) {
    throw new IllegalStateException("GraphFlux done result must use String keys", e);
}

Prevention

When it happens

Trigger: Calling Data.done(map) / returning a GraphFlux done result whose map uses non-String keys — typically a Map<Integer,Object>, Map<Enum,Object>, or a map built from untyped upstream data (e.g. parsed structures with non-string keys).

Common situations: Reusing domain maps keyed by IDs/enums as state updates; JSON/YAML parsers producing maps with non-string keys; building the done-map programmatically with mixed key types.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/041b9794feab75c1. Report an issue: GitHub.