spring-projects/spring-ai · error · IllegalStateException
Conversion from JSON to %s failed
Error message
Conversion from JSON to %s failed
What it means
When building tool-call messages, OpenAiChatModel.createRequest() deserializes the raw JSON arguments string of each tool call into a Map<String,Object> (MAP_TYPE_REF) and converts it to JsonValue additional properties. If the arguments string is not valid JSON, Jackson throws JsonProcessingException and the model wraps it in an IllegalStateException with this message, since the map type name is formatted into the text.
Source
Thrown at models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java:662
.stream()
.map(toolCall -> {
ChatCompletionMessageFunctionToolCall.Builder toolCallBuilder = ChatCompletionMessageFunctionToolCall
.builder()
.id(toolCall.id())
.function(ChatCompletionMessageFunctionToolCall.Function.builder()
.name(toolCall.name())
.arguments(toolCall.arguments())
.build());
String jsonProps = toolCallAdditionalProperties.get(toolCall.id());
if (StringUtils.hasText(jsonProps)) {
Map<String, JsonValue> additionalProperties = new LinkedHashMap<>();
try {
objectMapper.readValue(jsonProps, MAP_TYPE_REF)
.forEach((k, v) -> additionalProperties.put(k, JsonValue.from(v)));
}
catch (JsonProcessingException ex) {
throw new IllegalStateException("Conversion from JSON to %s failed"
.formatted(MAP_TYPE_REF.getType().getTypeName()), ex);
}
toolCallBuilder.putAllAdditionalProperties(additionalProperties);
}
return ChatCompletionMessageToolCall.ofFunction(toolCallBuilder.build());
})
.toList();
builder.toolCalls(toolCalls);
}
// Replay reasoning content only when present - plain OpenAI is
// unaffected
Object reasoningContent = assistantMessage.getMetadata().get(REASONING_CONTENT);
if (reasoningContent instanceof String reasoning && StringUtils.hasText(reasoning)) {
// "reasoning_content" is the wire field; REASONING_CONTENT is the
// metadata key
builder.putAdditionalProperty("reasoning_content", JsonValue.from(reasoning));View on GitHub (pinned to 98a7beda4f)
Solutions
- Validate the tool call's arguments string parses as JSON before adding the message to history (e.g. objectMapper.readValue(args, Map.class) in a pre-check).
- Truncate or drop the malformed tool-call message from conversation history and ask the model to retry the tool call.
- Check max_tokens/output limits that truncate tool-call JSON and increase them.
- If arguments are stored in memory/persistence, verify they were not double-encoded or altered; re-serialize the original Map instead of a string round-trip.
- Inspect the wrapped JsonProcessingException cause to locate the exact JSON syntax error.
Example fix
// before
messages.add(new AssistantMessage.ToolCall(id, "function", name, invalidArgsJson));
// after
String args = invalidArgsJson;
try {
objectMapper.readTree(args); // fail fast with a clear message
}
catch (JsonProcessingException e) {
args = "{\"_raw\":\"\"}"; // or drop/re-request the tool call
}
messages.add(new AssistantMessage.ToolCall(id, "function", name, args)); Defensive patterns
Strategy: try-catch
Validate before calling
// Validate tool-call arguments JSON before adding to conversation history
void requireValidJson(String args) {
try { objectMapper.readTree(args); }
catch (JsonProcessingException e) {
throw new IllegalArgumentException("Tool call arguments are not valid JSON", e);
}
} Type guard
boolean isValidJson(String s) {
if (s == null || s.isBlank()) return false;
try { objectMapper.readTree(s); return true; }
catch (JsonProcessingException e) { return false; }
} Try / catch
try {
ChatResponse response = chatModel.call(promptWithHistory);
} catch (IllegalStateException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Conversion from JSON to")) {
// drop/repair the malformed tool-call message in history and retry
} else throw e;
} Prevention
- Validate every tool-call arguments string with a JSON parse before persisting it to chat memory.
- Set max_tokens high enough that tool-call JSON is never truncated mid-string.
- Store the parsed arguments Map rather than round-tripping an arguments String in persistence.
- When replaying history, drop tool-call messages whose arguments fail JSON validation and re-prompt the model.
- Increase tool JSON reliability by keeping tool schemas simple (avoid deeply nested required fields).
When it happens
Trigger: An assistant tool-call message whose function arguments string is malformed JSON — e.g. truncated arguments, arguments produced by an LLM that emitted invalid JSON, or manually constructed ToolResponse/assistant messages with arguments set to non-JSON text — while re-creating a ChatCompletionRequest for the follow-up round trip.
Common situations: Feeding back conversation history where the model emitted invalid/truncated tool-call JSON (long arguments cut off by max_tokens); persisting chat memory with escaped or double-encoded JSON and replaying it; hand-crafting tool call messages in tests; switching models whose tool-call JSON is malformed.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- Failed to parse JSON:
- Failed to convert map to JSON
- Unexpected JSON token %s within the array!
- Unexpected JSON token %s!
- Cannot deserialize ThinkOption from token:
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/feb17a16debf1701.
Report an issue: GitHub.