{"record":{"id":"4ec9bbe4427a40e1","repo":"github/copilot-sdk","slug":"failed-to-serialize-tool-result-exc","errorCode":null,"errorMessage":"Failed to serialize tool result: {exc}","messagePattern":"Failed to serialize tool result: (.+?)","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"python/copilot/tools.py","lineNumber":387,"sourceCode":"\n    # Everything else gets JSON-serialized (with common Python and Pydantic values)\n    def default(obj: Any) -> Any:\n        if isinstance(obj, BaseModel):\n            return obj.model_dump(mode=\"json\")\n        if isinstance(obj, (date, datetime, time)):\n            return obj.isoformat()\n        if isinstance(obj, (Decimal, UUID)):\n            return str(obj)\n        if isinstance(obj, Enum):\n            return obj.value\n        if isinstance(obj, set):\n            return list(obj)\n        raise TypeError(f\"Object of type {type(obj).__name__} is not JSON serializable\")\n\n    try:\n        json_str = json.dumps(result, default=default)\n    except (TypeError, ValueError) as exc:\n        raise TypeError(f\"Failed to serialize tool result: {exc}\") from exc\n\n    return ToolResult(\n        text_result_for_llm=json_str,\n        result_type=\"success\",\n    )\n\n\ndef convert_mcp_call_tool_result(call_result: dict[str, Any]) -> ToolResult:\n    \"\"\"Convert an MCP CallToolResult dict into a ToolResult.\"\"\"\n    text_parts: list[str] = []\n    binary_results: list[ToolBinaryResult] = []\n\n    for block in call_result[\"content\"]:\n        block_type = block.get(\"type\")\n        if block_type == \"text\":\n            text = block.get(\"text\", \"\")\n            if isinstance(text, str):\n                text_parts.append(text)","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/python/copilot/tools.py#L369-L405","documentation":"_normalize_result converts a tool handler's return value into a ToolResult. Values that are not None, str, or ToolResult are JSON-serialized; if json.dumps fails (TypeError/ValueError), this TypeError is raised naming the unserializable object. Only basic Python types, Pydantic models, datetime/date/time, Decimal, UUID, Enum, and set are auto-converted.","triggerScenarios":"A tool handler registered via define_tool returns an object json.dumps cannot serialize and the default converter doesn't handle — e.g. a custom class, bytes, a numpy array, an open file object, or a dict containing such values.","commonSituations":"Returning ORM/database rows or dataclass instances directly; returning bytes from a file reader; returning numpy/tensor values; returning objects with circular references (ValueError); forgetting to call .model_dump() on non-Pydantic model objects.","solutions":["Return a JSON-compatible value from the handler: dict, list, str, int, float, bool, None.","Convert unsupported objects before returning: bytes -> base64/text, custom classes -> dataclasses.asdict(obj) or obj.dict(), numpy -> .tolist().","Return a ToolResult explicitly with text_result_for_llm set to a pre-serialized string.","Wrap the handler result yourself and convert Pydantic models with model_dump(mode='json') if they contain exotic field types."],"exampleFix":"// before\ndef read_file(path: str):\n    return open(path, 'rb').read()  # bytes -> TypeError\n// after\ndef read_file(path: str):\n    import base64\n    return {'encoding': 'base64', 'content': base64.b64encode(open(path, 'rb').read()).decode()}","handlingStrategy":"type-guard","validationCode":"import json\ndef jsonable(v):\n    try:\n        json.dumps(v, default=lambda o: o.model_dump(mode='json') if hasattr(o, 'model_dump') else str(o))\n        return True\n    except (TypeError, ValueError):\n        return False","typeGuard":"def is_json_serializable(v) -> bool:\n    if v is None or isinstance(v, (str, int, float, bool, list, dict)):\n        return True\n    if isinstance(v, (bytes, set, object)) and not hasattr(v, 'model_dump'):\n        try:\n            json.dumps(v)\n            return True\n        except (TypeError, ValueError):\n            return False\n    return hasattr(v, 'model_dump')","tryCatchPattern":"try:\n    tool = define_tool(name='x', handler=handler)\nexcept TypeError as exc:\n    if 'Failed to serialize tool result' in str(exc):\n        log.error('handler returned non-JSON value: %s', exc)\n    raise","preventionTips":["Return only plain JSON types (dict/list/str/num/bool/None) or ToolResult from handlers.","Convert bytes and custom classes to dicts/strings before returning.","Call model_dump(mode='json') on Pydantic models with exotic field types.","Add a smoke test that invokes each tool handler and asserts the result serializes."],"tags":["python","json","serialization","tools"],"backgroundTag":"json-serialization-failed","analyzedSha":"cd8cf15dc3f9e762615790aaed0a771a0f392755","analyzedAt":"2026-09-09T18:32:31.973Z","contentChangedAt":"2026-09-09T18:32:31.973Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}