{"record":{"id":"219ea4bdb8d5266d","repo":"hiyouga/LlamaFactory","slug":"tools-is-not-valid-json-tools-r","errorCode":null,"errorMessage":"tools is not valid JSON: {tools!r}","messagePattern":"tools is not valid JSON: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llamafactory/v1/core/rendering/rendering.py","lineNumber":78,"sourceCode":"\n    template_caller = processor if is_multimodal else tokenizer\n    if not getattr(template_caller, \"chat_template\", None):\n        template_caller.chat_template = _FALLBACK_CHATML_JINJA\n\n    # 0. Neutralize special-token strings in user-controlled text (no-op for normal data).\n    specials = _special_token_strings(tokenizer)\n    special_ids = {tid for tid, t in tokenizer.added_tokens_decoder.items() if getattr(t, \"special\", False)}\n    messages = _escape_special_in_messages(messages, specials, special_ids, tokenizer)\n\n    hf_messages = _to_hf_messages(messages, is_multimodal=is_multimodal)\n\n    tools_parsed = None\n    if tools:\n        tools = _escape_special(tools, specials, special_ids, tokenizer)  # E3: tools text is user-controlled\n        try:\n            tools_parsed = json.loads(tools)\n        except json.JSONDecodeError as e:\n            raise ValueError(f\"tools is not valid JSON: {tools!r}\") from e\n        if not isinstance(tools_parsed, list):\n            tools_parsed = [tools_parsed]\n\n    if not is_generate and hf_messages and hf_messages[-1][\"role\"] == \"assistant\":\n        kwargs[\"enable_thinking\"] = bool(hf_messages[-1].get(\"reasoning_content\"))\n\n    def _encode(hf_msgs: list[dict], src_msgs: list[Message], add_generation_prompt: bool):\n        \"\"\"Render + tokenize, expanding media via the processor. Returns (input_ids, mm_outputs).\"\"\"\n        text = template_caller.apply_chat_template(\n            hf_msgs, tokenize=False, add_generation_prompt=add_generation_prompt, tools=tools_parsed, **kwargs\n        )\n        if is_multimodal and _count_media_in_messages(src_msgs) != (0, 0, 0):\n            images, videos, audios = _extract_media_from_messages(src_msgs)\n            # Every placeholder must come from a media block (escaping broke any literal ones).\n            _check_placeholder_counts(processor, text, len(images), len(videos), len(audios))\n            proc_kwargs = {\"return_tensors\": \"pt\"}\n            if images:\n                proc_kwargs[\"images\"] = images","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/hiyouga/LlamaFactory/blob/f28afaf6355af515454dfb16c97d728307c93897/src/llamafactory/v1/core/rendering/rendering.py#L60-L96","documentation":"The optional `tools` argument to the renderer must be a JSON string (it is escaped like user text, then json.loads'd). If parsing fails, this error is raised with the offending string repr. Note the escape step inserts zero-width spaces into the tools text only for self-validation; the parsed object is still required to be valid JSON.","triggerScenarios":"Calling render_messages/messages_to_model_input with tools=... where tools is a Python list/dict (not serialized), a malformed JSON string, or a string whose JSON was broken by embedding special-token text that the escaper mutated.","commonSituations":"Passing json-output of a tool-definition builder with trailing commas; double-encoding (tools=json.dumps(json.dumps(x))); tool schemas copied from docs containing smart quotes.","solutions":["Pass tools as a JSON string produced by json.dumps(tool_list)","If tools is already a list/dict, serialize it before the call","Validate with json.loads(tools) in a unit test or preprocessing assert before training"],"exampleFix":"# before\nrenderer.render_messages(messages, tools=[{\"type\": \"function\", \"function\": {...}}])\n\n# after\nimport json\nrenderer.render_messages(messages, tools=json.dumps([{\"type\": \"function\", \"function\": {...}}]))","handlingStrategy":"validation","validationCode":"import json\n\ndef prepare_tools(tools) -> str:\n    if isinstance(tools, (list, dict)):\n        tools = json.dumps(tools)\n    json.loads(tools)  # fail early, clearly\n    return tools","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Build tools strings with json.dumps from Python objects","Keep tool schemas in .json files loaded and validated once at startup"],"tags":["tools","json","function-calling","rendering"],"backgroundTag":null,"analyzedSha":"f28afaf6355af515454dfb16c97d728307c93897","analyzedAt":"2026-08-14T21:57:28.298Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}