{"record":{"id":"041b9794feab75c1","repo":"alibaba/spring-ai-alibaba","slug":"graphflux-done-result-map-keys-must-be-string","errorCode":null,"errorMessage":"GraphFlux done result map keys must be String","messagePattern":"GraphFlux done result map keys must be String","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/executor/NodeExecutor.java","lineNumber":811,"sourceCode":"\t\t\t\t\treturn copyStateMap(resultMap);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMap<String, Object> state = new HashMap<>();\n\t\tstate.put(graphFluxStateKey(graphFlux), lastData);\n\t\treturn state;\n\t}\n\n\tprivate static String graphFluxStateKey(GraphFlux<?> graphFlux) {\n\t\treturn StringUtils.hasText(graphFlux.getKey()) ? graphFlux.getKey() : \"result\";\n\t}\n\n\tprivate static Map<String, Object> copyStateMap(Map<?, ?> resultMap) {\n\t\tMap<String, Object> state = new HashMap<>();\n\t\tfor (Map.Entry<?, ?> entry : resultMap.entrySet()) {\n\t\t\tif (!(entry.getKey() instanceof String key)) {\n\t\t\t\tthrow new IllegalArgumentException(\"GraphFlux done result map keys must be String\");\n\t\t\t}\n\t\t\tstate.put(key, entry.getValue());\n\t\t}\n\t\treturn state;\n\t}\n\n\t/**\n\t * Checks interruptAfter hook for streaming nodes using the pre-merge state.\n\t * <p>\n\t * This method must be called <strong>before</strong> the streaming state updates are\n\t * merged into the {@link OverAllState} to keep semantics consistent with the\n\t * non-streaming interruptAfter hook.\n\t * @param context the graph runner context\n\t * @param actionResult the streaming node action result (state delta) passed to interruptAfter\n\t * @return interruption metadata if the hook triggers\n\t */\n\tprivate Optional<InterruptionMetadata> interruptAfterForStreaming(GraphRunnerContext context,\n\t\t\tMap<String, Object> actionResult) {","sourceCodeStart":793,"sourceCodeEnd":829,"githubUrl":"https://github.com/alibaba/spring-ai-alibaba/blob/f82da0b50f35744c13968191be2b1cd2452ef550/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/executor/NodeExecutor.java#L793-L829","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Convert all keys to String before calling done: map.entrySet() -> Map.of(String.valueOf(k), v).","If keys are enums or IDs, map them to their configured state property names (String) first.","Add a pre-check validating the map is Map<String, ?> before passing it to Data.done()."],"exampleFix":"// before\nMap<Integer, Object> byId = ...;\nreturn Data.done(byId);\n// after\nMap<String, Object> state = byId.entrySet().stream()\n    .collect(Collectors.toMap(e -> String.valueOf(e.getKey()), Map.Entry::getValue));\nreturn Data.done(state);","handlingStrategy":"type-guard","validationCode":"for (Map.Entry<?, ?> e : doneMap.entrySet()) {\n    if (!(e.getKey() instanceof String)) {\n        throw new IllegalStateException(\"done map key not a String: \" + e.getKey());\n    }\n}","typeGuard":"boolean hasStringKeys(Map<?, ?> m) {\n    return m.keySet().stream().allMatch(String.class::isInstance);\n}","tryCatchPattern":"try {\n    state = copyStateMap(resultMap);\n} catch (IllegalArgumentException e) {\n    throw new IllegalStateException(\"GraphFlux done result must use String keys\", e);\n}","preventionTips":["Build done maps as Map<String, Object> explicitly instead of reusing domain-keyed maps.","Convert ID/enum keys to their state property names before calling Data.done().","Add an assertion util that validates String keys on any map handed to the graph state."],"tags":["streaming","type-mismatch","state-map","keys"],"backgroundTag":"type-mismatch","analyzedSha":"f82da0b50f35744c13968191be2b1cd2452ef550","analyzedAt":"2026-09-09T15:32:42.421Z","contentChangedAt":"2026-09-09T15:32:42.421Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}