{"record":{"id":"2b0a83232073d945","repo":"spring-projects/spring-ai","slug":"failed-to-parse-toolchoice-json","errorCode":null,"errorMessage":"Failed to parse toolChoice JSON: ","messagePattern":"Failed to parse toolChoice JSON: ","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java","lineNumber":905,"sourceCode":"\t\t\t}\n\t\t\telse if (requestOptions.getToolChoice() instanceof String json) {\n\t\t\t\tif (json.equals(\"auto\")) {\n\t\t\t\t\tbuilder.toolChoice(ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO));\n\t\t\t\t}\n\t\t\t\telse if (json.equals(\"none\")) {\n\t\t\t\t\tbuilder.toolChoice(ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.NONE));\n\t\t\t\t}\n\t\t\t\telse if (json.equals(\"required\")) {\n\t\t\t\t\tbuilder.toolChoice(\n\t\t\t\t\t\t\tChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.REQUIRED));\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tvar node = JacksonUtils.getDefaultJsonMapper().readTree(json);\n\t\t\t\t\t\tbuilder.toolChoice(parseToolChoice(node));\n\t\t\t\t\t}\n\t\t\t\t\tcatch (Exception e) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"Failed to parse toolChoice JSON: \" + json, e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add extraBody parameters as additional body properties for OpenAI-compatible\n\t\t// providers\n\t\tif (requestOptions.getExtraBody() != null && !requestOptions.getExtraBody().isEmpty()) {\n\t\t\tMap<String, JsonValue> extraParams = requestOptions.getExtraBody()\n\t\t\t\t.entrySet()\n\t\t\t\t.stream()\n\t\t\t\t.collect(Collectors.toMap(Map.Entry::getKey, entry -> JsonValue.from(entry.getValue())));\n\t\t\tbuilder.additionalBodyProperties(extraParams);\n\t\t}\n\n\t\treturn builder.build();\n\t}\n","sourceCodeStart":887,"sourceCodeEnd":923,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java#L887-L923","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\noptions.setToolChoice(\"get_weather\"); // not auto/none/required, not valid JSON\n\n// after\noptions.setToolChoice(\"{\\\"type\\\":\\\"function\\\",\\\"function\\\":{\\\"name\\\":\\\"get_weather\\\"}}\");\n// or type-safe:\noptions.setToolChoice(ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO));","handlingStrategy":"validation","validationCode":"public static Object validateToolChoice(Object toolChoice) {\n    if (toolChoice instanceof String s && !s.equals(\"auto\") && !s.equals(\"none\") && !s.equals(\"required\")) {\n        try {\n            new com.fasterxml.jackson.databind.ObjectMapper().readTree(s);\n        } catch (Exception e) {\n            throw new IllegalArgumentException(\"toolChoice must be auto/none/required or valid JSON\", e);\n        }\n    }\n    return toolChoice;\n}","typeGuard":"boolean isValidToolChoice(Object tc) {\n    if (tc instanceof ChatCompletionToolChoiceOption) return true;\n    if (tc instanceof String s) {\n        return s.equals(\"auto\") || s.equals(\"none\") || s.equals(\"required\") || isValidJson(s);\n    }\n    return false;\n}","tryCatchPattern":"try {\n    return chatModel.call(prompt);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"Failed to parse toolChoice JSON:\")) {\n        logger.warn(\"Falling back to tool_choice=auto; original value: {}\", e.getMessage());\n        prompt.getOptions().setToolChoice(\"auto\");\n        return chatModel.call(prompt);\n    }\n    throw e;\n}","preventionTips":["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."],"tags":["openai","spring-ai","tool-choice","json-parse","illegal-argument"],"backgroundTag":"json-parse-error","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}