spring-projects/spring-ai · error · IllegalArgumentException
Failed to parse toolChoice JSON:
Error message
Failed to parse toolChoice JSON:
What it means
OpenAiChatOptions.toolChoice accepts a ChatCompletionToolChoiceOption or a String. A String that is not exactly "auto", "none", or "required" is treated as a JSON document and parsed with Jackson's readTree; if that parse fails (or a subsequent step throws), createRequest wraps it in this IllegalArgumentException with the raw string in the message and the parse exception as cause. It protects against passing a malformed toolChoice value that would produce an invalid API request.
Source
Thrown at models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java:905
}
else if (requestOptions.getToolChoice() instanceof String json) {
if (json.equals("auto")) {
builder.toolChoice(ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO));
}
else if (json.equals("none")) {
builder.toolChoice(ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.NONE));
}
else if (json.equals("required")) {
builder.toolChoice(
ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.REQUIRED));
}
else {
try {
var node = JacksonUtils.getDefaultJsonMapper().readTree(json);
builder.toolChoice(parseToolChoice(node));
}
catch (Exception e) {
throw new IllegalArgumentException("Failed to parse toolChoice JSON: " + json, e);
}
}
}
}
// Add extraBody parameters as additional body properties for OpenAI-compatible
// providers
if (requestOptions.getExtraBody() != null && !requestOptions.getExtraBody().isEmpty()) {
Map<String, JsonValue> extraParams = requestOptions.getExtraBody()
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> JsonValue.from(entry.getValue())));
builder.additionalBodyProperties(extraParams);
}
return builder.build();
}
View on GitHub (pinned to 98a7beda4f)
Solutions
- If you meant a named function, pass a valid JSON object string: {"type":"function","function":{"name":"yourFunction"}}.
- If you meant one of the keywords, pass exactly "auto", "none", or "required" (lowercase) — anything else is parsed as JSON.
- Validate the string with JacksonUtils.getDefaultJsonMapper().readTree(json) in a try/catch before setting it, and check the cause for the exact syntax error.
- Alternatively set toolChoice to a ChatCompletionToolChoiceOption built from the OpenAI SDK (e.g. ChatCompletionToolChoiceOption.ofAuto(...)) to bypass string parsing entirely.
Example fix
// before
options.setToolChoice("get_weather"); // not auto/none/required, not valid JSON
// after
options.setToolChoice("{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}}");
// or type-safe:
options.setToolChoice(ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO)); Defensive patterns
Strategy: validation
Validate before calling
public static Object validateToolChoice(Object toolChoice) {
if (toolChoice instanceof String s && !s.equals("auto") && !s.equals("none") && !s.equals("required")) {
try {
new com.fasterxml.jackson.databind.ObjectMapper().readTree(s);
} catch (Exception e) {
throw new IllegalArgumentException("toolChoice must be auto/none/required or valid JSON", e);
}
}
return toolChoice;
} Type guard
boolean isValidToolChoice(Object tc) {
if (tc instanceof ChatCompletionToolChoiceOption) return true;
if (tc instanceof String s) {
return s.equals("auto") || s.equals("none") || s.equals("required") || isValidJson(s);
}
return false;
} Try / catch
try {
return chatModel.call(prompt);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Failed to parse toolChoice JSON:")) {
logger.warn("Falling back to tool_choice=auto; original value: {}", e.getMessage());
prompt.getOptions().setToolChoice("auto");
return chatModel.call(prompt);
}
throw e;
} Prevention
- Prefer ChatCompletionToolChoiceOption objects from the SDK over raw strings.
- For named functions, always pass a full JSON object string: {"type":"function","function":{"name":"..."}} — a bare function name is not valid JSON.
- Validate non-keyword strings with readTree() before setting them.
- Keep keyword strings lowercase and exact: auto, none, required.
When it happens
Trigger: Calling with OpenAiChatOptions.toolChoice set to a String that is neither auto/none/required nor valid JSON — e.g. a bare function name like "get_weather" (unquoted is invalid JSON), a function-choice object written in non-JSON syntax, a single-quoted pseudo-JSON string, or an empty/whitespace string.
Common situations: Passing the desired function name directly as the toolChoice string instead of a JSON object like {"type":"function","function":{"name":"get_weather"}}; copying a toolChoice value from Python/JS client code with different quoting; building the JSON by hand and breaking the quotes; property-file configuration injecting an unescaped value.
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.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Unknown tool_choice type:
- Unsupported message type:
- Failed to parse JSON schema:
- Unsupported response format type:
- Unsupported media type: . Supported types are: images (image
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/2b0a83232073d945.
Report an issue: GitHub.