Skip to content

Serialize tool results with Pydantic's JSON mode - #2204

Open
thejesh23 wants to merge 1 commit into
github:mainfrom
thejesh23:fix/tool-result-non-primitive-serialization
Open

Serialize tool results with Pydantic's JSON mode#2204
thejesh23 wants to merge 1 commit into
github:mainfrom
thejesh23:fix/tool-result-non-primitive-serialization

Conversation

@thejesh23

Copy link
Copy Markdown

Fixes #2203

What's wrong

_normalize_result's default hook calls model_dump(), which uses the default mode="python". That leaves datetime, date, UUID, Decimal, Enum and set fields as native Python objects, so json.dumps passes them straight back into the same hook, which doesn't recognise them and raises TypeError.

That raise happens inside wrapped_handler's try block, which converts any exception into the deliberately redacted failure result. So a tool returning a perfectly ordinary model:

class Issue(BaseModel):
    id: uuid.UUID
    created_at: datetime.datetime

reports "Invoking this tool produced an error. Detailed information is not available." to the model on every call, and the tool author sees no indication of why.

The fix

model_dump(mode="json") makes Pydantic coerce each field to a JSON-native type before json.dumps sees it. Models whose fields are all JSON primitives serialize exactly as before, so this is backwards compatible.

This matches what the TypeScript SDK already does via JSON.stringify (JS Date implements toJSON()).

Verification

Added test_pydantic_model_with_non_primitive_fields_is_serialized to the existing TestNormalizeResult class, covering UUID, datetime, Decimal and StrEnum in one model.

It fails on the unfixed code with the reported error:

TypeError: Failed to serialize tool result: Object of type UUID is not JSON serializable
copilot/tools.py:360: TypeError

and passes with the change. Full local run:

  • uv run pytest test_tools.py41 passed (40 before, plus the new test)
  • uv run pytest --ignore=e2e --ignore=test_client.py --ignore=test_commands_and_elicitation.py --ignore=test_e2e_harness_cli_path.py184 passed
  • uv run ruff check . → All checks passed
  • uv run ruff format --check → already formatted

The four ignored modules fail at collection with RuntimeError: CLI not found for tests in my environment; I confirmed they fail identically on unmodified main, so that is environmental and unrelated to this change.

Scope

Deliberately minimal — one line of behaviour change plus a test. For fuller parity with the TypeScript SDK you may also want fallbacks in default for dataclasses.asdict, datetime/date/time.isoformat(), UUID/Decimalstr, Enum.value and setlist, so a plain dict carrying those types works too. I left that out to keep the diff focused — happy to add it if you'd prefer it here.

`_normalize_result`'s `default` hook called `model_dump()`, which uses
the default `mode="python"`. That leaves `datetime`, `date`, `UUID`,
`Decimal`, `Enum` and `set` fields as native Python objects, so
`json.dumps` hands them straight back to the same hook, which does not
recognise them and raises `TypeError`.

The raise happens inside `wrapped_handler`'s try block, which converts
any exception into the deliberately redacted failure result. A tool
returning a perfectly ordinary model such as

    class Issue(BaseModel):
        id: uuid.UUID
        created_at: datetime.datetime

therefore reports "Invoking this tool produced an error. Detailed
information is not available." to the model on every call, with no
indication to the author of what went wrong.

Switching to `model_dump(mode="json")` makes Pydantic coerce each field
to a JSON-native type before `json.dumps` sees it, which matches what
the TypeScript SDK already does via `JSON.stringify` (JS `Date`
implements `toJSON`). Models whose fields are all JSON primitives
serialize exactly as before.
@thejesh23
thejesh23 requested a review from a team as a code owner August 1, 2026 09:32
Copilot AI review requested due to automatic review settings August 1, 2026 09:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates Python tool-result normalization to serialize Pydantic models using JSON mode.

Changes:

  • Converts Pydantic-native values into JSON-compatible representations.
  • Adds regression coverage for UUID, datetime, Decimal, and enum fields.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
python/copilot/tools.py Uses Pydantic JSON-mode serialization.
python/test_tools.py Adds non-primitive field serialization coverage.

Comment thread python/test_tools.py
assert result.result_type == "success"

def test_pydantic_model_with_non_primitive_fields_is_serialized(self):
class IssueState(enum.StrEnum):
@thejesh23

Copy link
Copy Markdown
Author

Correction to the verification section above: I originally ran the suite without the shared test harness, so I reported a partial run (184 passed) and described the four uncollectable modules as environmental.

I've since followed the CONTRIBUTING step I'd skipped — cd test/harness && npm ci — and the full Python suite passes:

uv run pytest
743 passed, 7 skipped in 500.63s

Plus the rest of the CI gate from .github/workflows/python-sdk-tests.yml:

  • uv run ruff format --check . → 113 files already formatted
  • uv run ruff check → All checks passed
  • uv run ty check copilot → exit 0 (2 pre-existing warnings, in client.py:4306 and copilot_request_handler.py:462 — both untouched here, and identical on unmodified main)

So there are no outstanding failures on this branch. Apologies for the noise in the original description.

Observed on darwin / Python 3.12.13 / Node v26.5.0, against 6287d1ad.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: tool results containing datetime/UUID/Decimal/Enum are reported to the model as a tool failure

2 participants