github/copilot-sdk · error · TypeError
Failed to serialize tool result
Error message
Failed to serialize tool result: {exc} What it means
_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.
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.
Example fix
// before
def read_file(path: str):
return open(path, 'rb').read() # bytes -> TypeError
// after
def read_file(path: str):
import base64
return {'encoding': 'base64', 'content': base64.b64encode(open(path, 'rb').read()).decode()} Defensive patterns
Strategy: type-guard
Validate before calling
import json
def jsonable(v):
try:
json.dumps(v, default=lambda o: o.model_dump(mode='json') if hasattr(o, 'model_dump') else str(o))
return True
except (TypeError, ValueError):
return False Type guard
def is_json_serializable(v) -> bool:
if v is None or isinstance(v, (str, int, float, bool, list, dict)):
return True
if isinstance(v, (bytes, set, object)) and not hasattr(v, 'model_dump'):
try:
json.dumps(v)
return True
except (TypeError, ValueError):
return False
return hasattr(v, 'model_dump') Try / catch
try:
tool = define_tool(name='x', handler=handler)
except TypeError as exc:
if 'Failed to serialize tool result' in str(exc):
log.error('handler returned non-JSON value: %s', exc)
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.
Related errors
- Failed to serialize FFI JSON parameter.
- Expected a non-empty string value when writing
- Request handler must return a JSON-serializable value, got
- invalid tool name: must not be empty
- invalid tool name : tool names must match…
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/4ec9bbe4427a40e1.
Report an issue: GitHub.
Appendix: source
Thrown at python/copilot/tools.py:387
# Everything else gets JSON-serialized (with common Python and Pydantic values)
def default(obj: Any) -> Any:
if isinstance(obj, BaseModel):
return obj.model_dump(mode="json")
if isinstance(obj, (date, datetime, time)):
return obj.isoformat()
if isinstance(obj, (Decimal, UUID)):
return str(obj)
if isinstance(obj, Enum):
return obj.value
if isinstance(obj, set):
return list(obj)
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
try:
json_str = json.dumps(result, default=default)
except (TypeError, ValueError) as exc:
raise TypeError(f"Failed to serialize tool result: {exc}") from exc
return ToolResult(
text_result_for_llm=json_str,
result_type="success",
)
def convert_mcp_call_tool_result(call_result: dict[str, Any]) -> ToolResult:
"""Convert an MCP CallToolResult dict into a ToolResult."""
text_parts: list[str] = []
binary_results: list[ToolBinaryResult] = []
for block in call_result["content"]:
block_type = block.get("type")
if block_type == "text":
text = block.get("text", "")
if isinstance(text, str):
text_parts.append(text)View on GitHub (pinned to cd8cf15dc3)