spring-projects / spring-projects/spring-ai
Bedrock Converse: tool-call arguments serialized via Document.toString() produce invalid JSON, breaking tool execution when arguments contain newlines
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.5k
- Forks
- 2.9k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 6
Description
Bug description
When using the Bedrock Converse chat model with tool calling, BedrockProxyChatModel serializes the tool-use input by calling toString() on the AWS SDK Document:
var functionArguments = toolUseContentBlock.toolUse().input().toString();
Document.toString() is not a JSON serializer. It renders string values without escaping control characters (e.g. \n, \t). The resulting string is stored verbatim as AssistantMessage.ToolCall#arguments.
When the tool is executed, the arguments string is parsed back into a Map with a strict Jackson 3 mapper via the static JsonHelper.fromJsonToMap(...) (e.g. in SyncMcpToolCallback/AsyncMcpToolCallback). Jackson 3 rejects unescaped control characters inside string values, so the call fails:
tools.jackson.core.exc.StreamReadException: Illegal unquoted character
((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in string value
... (through reference chain: java.util.LinkedHashMap["text"])
at org.springframework.ai.util.JsonHelper.fromJsonToMap(JsonHelper.java)
at org.springframework.ai.mcp.SyncMcpToolCallback.call(SyncMcpToolCallback.java)
at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCall(...)
The bug is latent for single-line tool arguments and only surfaces when the model puts a newline (or other control character) into a tool argument — which is common and natural, e.g. a multi-line message body passed to a send-style tool.
Because JsonHelper is a static final field on the tool-callback classes, there is no injection point to relax parsing on the consuming side — the defect must be fixed where the malformed string is produced.
Environment
- Spring AI: 2.0.1 (also present on
main) - Module:
spring-ai-bedrock-converse - Jackson: 3.x (
tools.jackson.*), strict about unescaped control characters by default - Model: Anthropic Claude via Bedrock Converse (channel-agnostic; any tool with a string
argument containing a newline reproduces it)
Steps to reproduce
- Configure a
ChatClientwith the Bedrock Converse model and any tool that accepts a string parameter. - Prompt the model so it calls the tool with a multi-line string argument (containing
\n). - Tool execution fails with the
StreamReadExceptionabove; the request returns a 500.
Expected behavior
Tool-call arguments produced by BedrockProxyChatModel should be valid JSON regardless of the content of string values, so they round-trip through JsonHelper.fromJsonToMap(...) during tool execution.
Root cause
Document.toString() is used as if it were a JSON serializer. The Document is a structured value (map/list/string/number/boolean/null) and should be serialized with a real JSON writer. Note the request path already does the correct inverse:
ConverseApiUtils.convertObjectToDocument(jsonHelper.fromJsonToMap(toolCall.arguments())).
Proposed fix
Add the inverse converter ConverseApiUtils.convertDocumentToObject(Document) and serialize with the existing jsonHelper:
var functionArguments = jsonHelper
.toJson(ConverseApiUtils.convertDocumentToObject(toolUseContentBlock.toolUse().input()));
This escapes control characters correctly, is symmetric with the request path, and fixes the issue for all tools (not just MCP).
Alternatives considered
- Relax JSON parsing to allow unescaped control characters (e.g. enable
JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARSon the mapper used byJsonHelper, or leniently re-parse the arguments before execution). Rejected as the upstream fix: it treats the symptom rather than the cause, weakens strict JSON parsing framework-wide (hiding malformed data elsewhere), and assumesDocument.toString()is otherwise valid JSON, which is not a documented contract. It is only appropriate as a downstream workaround where the producing code cannot be changed (the staticJsonHelperleaves no seam in the callback layer).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in the spring-ai-bedrock-converse module at BedrockProxyChatModel.java around line 676, then inspect ConverseApiUtils and the existing jsonHelper conversion used on the request path. Verify that tool-use Documents containing newlines and other control characters produce valid JSON that round-trips through JsonHelper.fromJsonToMap during tool execution.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, java
- Domain
- ai, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100