github/copilot-sdk · error · ValueError

Request handler must return a JSON-serializable value, got

Error message

Request handler must return a JSON-serializable value, got {type(outcome).__name__}

What it means

The JSON-RPC dispatcher validates that every request handler returns a JSON-serializable value (dict, list, str, int, float, bool) or None. Returning None is treated as a valid empty response, but any other object (custom class, dataclass, set, datetime) is rejected before the response is sent. This guard prevents emitting responses that cannot be serialized into a JSON-RPC result.

Solutions

  1. Make the handler return only dict/list/str/int/float/bool/None values
  2. Convert custom objects before returning: call `.model_dump()`, `.to_dict()`, or wrap a tuple in list()
  3. If no response body is needed, return None explicitly (accepted) instead of a sentinel object
  4. Add a return-type annotation/adapter layer on handler registration so conversion happens centrally

Example fix

// before
def handle_status(params):
    return SessionStatus(active=True)  # dataclass

// after
def handle_status(params):
    return {"active": True, "state": SessionStatus(params).state}
Defensive patterns

Strategy: validation

Validate before calling

def is_json_safe(value):
    return value is None or isinstance(value, (dict, list, str, int, float, bool))

assert is_json_safe(my_handler(params)), "handler must return JSON-serializable value"

Type guard

def is_json_safe(value) -> bool:
    return value is None or isinstance(value, (dict, list, str, int, float, bool))

Try / catch

try:
    result = await client.request("my/method", params)
except ValueError as e:
    if "JSON-serializable" in str(e):
        result = coerce_to_json(handler(params))

Prevention

When it happens

Trigger: A handler registered for a JSON-RPC method returns an object that is not one of dict | list | str | int | float | bool and is not None, e.g. returning a dataclass, set, tuple, datetime, or None-wrapped custom type from `handler(params)` in _dispatch_request.

Common situations: Developers register a handler that returns a dataclass or ORM model directly, return a tuple instead of a list, or return a custom result wrapper object; Python allows any return value so this only surfaces at dispatch time.

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/70fa87e550f955db. Report an issue: GitHub.

Appendix: source

Thrown at python/copilot/_jsonrpc.py:482

            async def _await_outcome():
                try:
                    await outcome
                except Exception:  # pylint: disable=broad-except
                    logger.warning("Notification handler raised", exc_info=True)

            asyncio.create_task(_await_outcome())

    async def _dispatch_request(self, message: dict, handler: RequestHandler):
        try:
            params = message.get("params", {})
            outcome = handler(params)
            if inspect.isawaitable(outcome):
                outcome = await outcome
            if outcome is not None and not isinstance(
                outcome, dict | list | str | int | float | bool
            ):
                raise ValueError(
                    "Request handler must return a JSON-serializable value, "
                    f"got {type(outcome).__name__}"
                )
            await self._send_response(message["id"], outcome)
        except JsonRpcError as exc:
            logger.debug(
                "Error handling JSON-RPC method %s: %s", message.get("method", ""), exc.message
            )
            await self._send_error_response(message["id"], exc.code, exc.message, exc.data)
        except Exception as exc:  # pylint: disable=broad-except
            logger.debug(
                "Error handling JSON-RPC method %s: %s",
                message.get("method", ""),
                str(exc),
                exc_info=True,
            )
            await self._send_error_response(message["id"], -32603, str(exc), None)

View on GitHub (pinned to cd8cf15dc3)