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

  1. Return a JSON-compatible value from the handler: dict, list, str, int, float, bool, None.
  2. Convert unsupported objects before returning: bytes -> base64/text, custom classes -> dataclasses.asdict(obj) or obj.dict(), numpy -> .tolist().
  3. Return a ToolResult explicitly with text_result_for_llm set to a pre-serialized string.
  4. 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

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


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)