Skip to content

Fix hydration of mutually recursive schema types - #6137

Closed
wozhenbang2004 wants to merge 4 commits into
PrefectHQ:mainfrom
wozhenbang2004:fix/mutually-recursive-result-types
Closed

wozhenbang2004 wants to merge 4 commits into
PrefectHQ:mainfrom
wozhenbang2004:fix/mutually-recursive-result-types

Conversation

@wozhenbang2004

Copy link
Copy Markdown
Contributor

Tools returning mutually recursive Person.pet / Pet.owner models can send correct structured_content while the client's .data silently uses the wrong class several levels down. Without schema titles, distinct generated classes share the display name Root, and recursive references bind to that shared name.

Bind recursive references to each generated class's identity, resolve the complete graph, and only then share it through the class cache. Public class names remain unchanged. For the four-level result in #6135, the final pet retains its owner field:

result = await client.call_tool("get_person", {})
assert result.data.pet.owner.pet.name == "Max"
assert result.data.pet.owner.pet.owner is None

Fixes #6135.

🤖 Generated with OpenAI Codex.

Co-authored-by: Alperen Keleş 19610315+alpaylan@users.noreply.github.com

wozhenbang2004 and others added 2 commits October 28, 2026 22:22
Generated with OpenAI Codex.

Co-authored-by: Alperen Keleş <19610315+alpaylan@users.noreply.github.com>
Generated with OpenAI Codex.

Co-authored-by: Alperen Keleş <19610315+alpaylan@users.noreply.github.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-28T14:27:01.823207Z
We couldn’t complete this review. Comment @codex review to try again.
93961c2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request is awaiting maintainer assignment. Linked PRs stay open while maintainers triage the issue; this check remains failing until assignment.

Per CONTRIBUTING.md, an external PR must reference an issue that's assigned to its author. To get there:

  1. A maintainer will review the issue and decide whether to assign you. Please don't comment just to request assignment.

Once you're assigned and the link is present, this check is re-run automatically. Previously gate-closed PRs also reopen — no further action needed.

Maintainers: reopen this PR or remove the missing-issue-link label to bypass this check.

@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. client Related to the FastMCP client SDK or client-side functionality. labels Oct 28, 2026
@himanshuchawla0807-gif

Copy link
Copy Markdown
Contributor

Additional regression coverage for your fix at 93961c29742218b2e929ec145f49bf4b0f7398c8: a three-type cycle through an array (A → list[B] → C → A), covering dataclass/Pydantic generation and absent/shared titles. All four cases fail on current main d07877e062337842cfd46afef4efe85fc87bab28 because the returning A is validated as C; all four pass on your head. They also check distinct class identities, serialization round-trip, repeated cached conversion and input immutability.

With these cases added temporarily, uv run pytest tests/utilities/json_schema_type -n 0 -q gives 427 passed, 1 skipped on your head (Python 3.12.14, Pydantic 2.14.1). The test passes Ruff check/format. A separate Codex agent independently repeated the four base/head cases. This is scoped converter validation, not current-main composition, a full-suite/platform result or merge readiness; the assignment and fork-workflow approval gates remain separate.

Runnable coverage if useful alongside your two-type/client cases:

from copy import deepcopy
from typing import Any

import pytest
from pydantic import TypeAdapter

from fastmcp.utilities.json_schema_type import json_schema_to_type


@pytest.mark.parametrize("extra", [False, True], ids=["dataclass", "pydantic"])
@pytest.mark.parametrize("title", [None, "Record"], ids=["untitled", "same-title"])
def test_three_type_cycle_through_array(extra: bool, title: str | None) -> None:
    definitions: dict[str, Any] = {
        "A": {
            "type": "object",
            "properties": {
                "a": {"type": "string"},
                "children": {"type": "array", "items": {"$ref": "#/$defs/B"}},
            },
            "required": ["a", "children"],
        },
        "B": {
            "type": "object",
            "properties": {
                "b": {"type": "integer"},
                "next": {"$ref": "#/$defs/C"},
            },
            "required": ["b", "next"],
        },
        "C": {
            "type": "object",
            "properties": {
                "c": {"type": "boolean"},
                "next": {"$ref": "#/$defs/A"},
            },
            "required": ["c", "next"],
        },
    }
    for definition in definitions.values():
        if extra:
            definition["additionalProperties"] = True
        if title:
            definition["title"] = title
    schema = {
        "type": "object",
        "properties": {"node": {"$ref": "#/$defs/A"}},
        "required": ["node"],
        "$defs": definitions,
    }
    original = deepcopy(schema)
    value = {
        "node": {
            "a": "first",
            "children": [
                {"b": 7, "next": {"c": True, "next": {"a": "last", "children": []}}}
            ],
        }
    }
    generated = json_schema_to_type(schema)
    for _ in range(2):
        result = TypeAdapter(generated).validate_python(value)
        first = result.node
        middle = first.children[0]
        last = middle.next.next
        assert last.a == "last"
        assert last.children == []
        assert type(last) is type(first)
        assert len({type(first), type(middle), type(middle.next)}) == 3
        assert TypeAdapter(generated).dump_python(result) == value
        assert json_schema_to_type(schema) is generated
    assert schema == original

Generated and executed with OpenAI Codex. No human review before posting.

@wozhenbang2004

Copy link
Copy Markdown
Contributor Author

Thanks for checking this. The three-type cycle through an array is useful additional regression coverage.

Generated with OpenAI Codex.

Co-authored-by: Alperen Keleş <19610315+alpaylan@users.noreply.github.com>

@fastmcp-triage-reviewer fastmcp-triage-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated review by the FastMCP triage reviewer bot.

The identity-bound recursive references fix the reported silent hydration error at the schema-conversion layer. Nested Person/Pet values retain their correct fields and types, and the additional three-type array cycle also preserves distinct classes and serialization. No consequential defect was found in this pass.

Checks

All seven regression cases fail on the base and pass with this revision. Four array-cycle probes and 93 neighboring recursion/default/cache tests also pass.

@zzstoatzz zzstoatzz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Independent adversarial review passed for this exact revision. Mutually recursive schema identity and complete-graph caching passed independent mixed dataclass/Pydantic probes. Applicable local static checks and the combined recovery tests passed. This is code approval; current CI, contribution gate and repository merge requirements remain separate prerequisites.

@fastmcp-triage-reviewer fastmcp-triage-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated review by the FastMCP triage reviewer bot.

The converter fix preserves distinct recursive model shapes without changing public class names. Independent three-type array cycles also retain class identity and serialize correctly across mixed dataclass/Pydantic graphs. No actionable defect found; this is not a merge-readiness determination.

Checks

At this revision, seven hydration regressions and 15 cache tests pass. The seven regressions fail on the pinned baseline. All 16 independent mixed-model cycle probes pass; the workflow change was inspected but its corpus run was not executed.

@zzstoatzz

Copy link
Copy Markdown
Collaborator

thanks — the fix and tests landed as #6394 with you as co-author, without the schema-crash workflow commit so that change can be reviewed on its own. closing this in favor of that one.

@zzstoatzz zzstoatzz closed this Nov 10, 2026
zzstoatzz added a commit that referenced this pull request Nov 10, 2026
Fixes #6135. Supersedes #6137.

Co-authored-by: wozhenbang2004 <113896798+wozhenbang2004@users.noreply.github.com>
Co-authored-by: Alperen Keleş <19610315+alpaylan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
zzstoatzz added a commit that referenced this pull request Nov 10, 2026
* fix: keep additionalProperties on object schemas without a type (#6127)

Fixes #6124.

Co-authored-by: Alperen Keleş <19610315+alpaylan@users.noreply.github.com>
(cherry picked from commit 2a8f333)

* Fix hydration of mutually recursive schema types (#6394)

Fixes #6135. Supersedes #6137.

Co-authored-by: wozhenbang2004 <113896798+wozhenbang2004@users.noreply.github.com>
Co-authored-by: Alperen Keleş <19610315+alpaylan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0d7bb12)

* Isolate filesystem component construction failures (#6210)

Fixes #6209.

(cherry picked from commit f911b58)

* fix(config): apply tool transforms before tag filters on single servers (#6214)

Fixes #6196.

Co-authored-by: arjunprabhulal <11761445+arjunprabhulal@users.noreply.github.com>
(cherry picked from commit 02c121c)

* fix(tools): isolate mutable defaults in transformed tool calls (#6184)

Fixes #6183.

(cherry picked from commit b25a1c5)

* fix(openapi): make resource templates for non-identifier path parameters readable (#6187)

Fixes #6150.

Co-authored-by: alpaylan <19610315+alpaylan@users.noreply.github.com>
(cherry picked from commit 63f5718)

* Keep constraints on scalar schema references (#6041)

Fixes #6042.

(cherry picked from commit e5176c9)

* Document that a custom httpx_client_factory owns its timeout (#5923)

Fixes #5922.

(cherry picked from commit c9f64de)

* Stop PingMiddleware erroring when the client has no back-channel (#5901)

Fixes #5900.

(cherry picked from commit 044a519)

* Store sessionless client log levels on the connection (#5899)

Fixes #5898.

(cherry picked from commit c6e0c4d)

* Register templated MCPMixin resources as resource templates (#5895)

Fixes #5894.

(cherry picked from commit 0c9f5f7)

* Send the whole body when a body $ref with siblings cannot be flattened (#5893)

Fixes #5892.

(cherry picked from commit cad6074)

* Pass the transport to the fastmcp-remote proxy so it forwards server-initiated requests (#5885)

Fixes #5884.

(cherry picked from commit 285dd4c)

* fix(cli): forward path, stateless and log-level to SDK MCPServer servers (#5870)

Fixes #5869.

(cherry picked from commit 6dcec21)

* fix(client): preserve a caller-provided OAuth httpx client factory when verify is set (#5849)

Fixes #5848.

(cherry picked from commit b7c6568)

* fix(prompts): resolve postponed annotations when rendering (#5803)

Fixes #5801.

Co-authored-by: BlueX888 <140241684+BlueX888@users.noreply.github.com>
(cherry picked from commit 3bf92a6)

* Roll back the tool stamp when a tool registration is rejected (#5798)

Fixes #5797.

(cherry picked from commit d6566c5)

* fix(openapi): hydrate data for wrapped non-object responses with validate_output=False (#5784)

Fixes #5781.

Co-authored-by: BlueX888 <140241684+BlueX888@users.noreply.github.com>
(cherry picked from commit 6451faa)

* Carry prompt argument titles through PromptArgument and proxies (#5749)

Fixes #5748.

(cherry picked from commit 288485c)

* Relay the client's sampling result unchanged through proxies (#5747)

Fixes #5746.

(cherry picked from commit 19c349f)

* fix(json-schema): keep x-unsupported-pattern when a format is set (#5731)

Fixes #5729.

(cherry picked from commit dc9849d)

* fix(tasks): let async-generator tools run as background tasks (#5730)

Fixes #5728.

(cherry picked from commit c66e181)

* Honor Code Mode search limits after BM25 ranking (#5715)

Fixes #5712.

Co-authored-by: BlueX888 <140241684+BlueX888@users.noreply.github.com>
(cherry picked from commit ee71391)

* Keep the client timeout from capping the HTTP read timeout (#5690)

Fixes #5675.

(cherry picked from commit bf95d12)

* fix(auth): avoid rereading failed authorization forms (#5570)

Fixes #5569.

Co-authored-by: JordanAfolabiLaw <252951842+JordanAfolabiLaw@users.noreply.github.com>
(cherry picked from commit e9f0443)

* Fix unbounded Apps dev log retention (#5549)

Fixes #5548.

(cherry picked from commit 0a9a482)

* Bound error-stat cardinality in ErrorHandlingMiddleware (#5537)

Fixes #5536.

(cherry picked from commit 089ff09)

* fix(auth): preserve GitHub sessions across transient verification failures (#4881)

Fixes #4854.

Co-authored-by: benjohngf <7750978+benjohngf@users.noreply.github.com>
(cherry picked from commit 3c6fbf7)

* Expect the extended read timeout in the factory timeout test (#6395)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d8c4edf)

* fix(auth): keep revocation ownership separate from user identity (#6404)

Fixes #5800. Builds on #5807.

Co-authored-by: BlueX888 <140241684+BlueX888@users.noreply.github.com>
Co-authored-by: Luck <232011771+67-luck@users.noreply.github.com>
(cherry picked from commit cdec3a9)

* Authenticate and pin the schema download in the OpenAPI performance test (#6177)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a66dd71)

* Resolve deferred defaults by alias and keep unverified GitHub scopes out of the cache

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e064736)

* Revert "Roll back the tool stamp when a tool registration is rejected (#5798)"

This reverts commit 36a1264.

* Revert "Keep constraints on scalar schema references (#6041)"

This reverts commit 1ffb3e6.

* Revert "fix(json-schema): keep x-unsupported-pattern when a format is set (#5731)"

This reverts commit 9da9835.

---------

Co-authored-by: zgy <101915083+bfs2021@users.noreply.github.com>
Co-authored-by: Alperen Keleş <19610315+alpaylan@users.noreply.github.com>
Co-authored-by: wozhenbang2004 <113896798+wozhenbang2004@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: ppy <285454460@qq.com>
Co-authored-by: ayush <ayushkumarsaha32@gmail.com>
Co-authored-by: arjunprabhulal <11761445+arjunprabhulal@users.noreply.github.com>
Co-authored-by: Priyanshu Gupta <priyanshug26005@gmail.com>
Co-authored-by: Shayan Ali <shayan.ali8799@gmail.com>
Co-authored-by: Asem ahmed <94972604+asasemahmed@users.noreply.github.com>
Co-authored-by: 哈基米 <140241684+BlueX888@users.noreply.github.com>
Co-authored-by: Luck <2115759892@qq.com>
Co-authored-by: Winoren <158550716+Winoren@users.noreply.github.com>
Co-authored-by: nosmile99 <147114285+nosmile99@users.noreply.github.com>
Co-authored-by: Aarnav Noble <98567595+AarnavNoble@users.noreply.github.com>
Co-authored-by: 一片一片 <3037678834@qq.com>
Co-authored-by: Junpei Zhang <1259636180@qq.com>
Co-authored-by: JordanAfolabiLaw <252951842+JordanAfolabiLaw@users.noreply.github.com>
Co-authored-by: mika <spacexstarship01@outlook.com>
Co-authored-by: sui-ni2 <loxri127@gmail.com>
Co-authored-by: benjohngf <7750978+benjohngf@users.noreply.github.com>
Co-authored-by: Luck <232011771+67-luck@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. client Related to the FastMCP client SDK or client-side functionality. missing-issue-link

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mutually recursive Pydantic return types are hydrated as the wrong class in result.data

3 participants