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

  1. If you meant a named function, pass a valid JSON object string: {"type":"function","function":{"name":"yourFunction"}}.
  2. If you meant one of the keywords, pass exactly "auto", "none", or "required" (lowercase) — anything else is parsed as JSON.
  3. Validate the string with JacksonUtils.getDefaultJsonMapper().readTree(json) in a try/catch before setting it, and check the cause for the exact syntax error.
  4. 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

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.

Related errors


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