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

  1. 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).
  2. Truncate or drop the malformed tool-call message from conversation history and ask the model to retry the tool call.
  3. Check max_tokens/output limits that truncate tool-call JSON and increase them.
  4. 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.
  5. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/feb17a16debf1701. Report an issue: GitHub.