Skip to content

[Bug] _resolve_json_schema_reference infinitely recurses on self-referential models and KeyErrors on non-#/$defs refs — breaks dspy.Tool construction and MCP tool conversion #10327

Description

@KaiOnCode

What happened?

_resolve_json_schema_reference (dspy/adapters/types/tool.py:517) expands JSON-Schema $refs with no cycle detection and a lookup that assumes every ref's last path segment exists in $defs. Three distinct failure modes follow, all reachable from public API:

1. Self-referential pydantic model as a tool argument → RecursionError

Recursive input schemas are legal JSON Schema, and pydantic emits exactly that shape for self-referencing models. Construction of the tool crashes:

import pydantic, dspy

class TreeNode(pydantic.BaseModel):
    name: str
    children: list["TreeNode"] = []

TreeNode.model_rebuild()

def organize(tree: TreeNode) -> str:
    """Summarize a tree."""
    return "ok"

dspy.Tool(organize)  # RecursionError

Same for the MCP path: convert_mcp_toolconvert_input_schema_to_tool_args_resolve_json_schema_reference — any MCP server with a recursive input schema makes tool listing fail. Measured on main (59ce760), Python 3.11, macOS.

Note the project already solves this for prompts: _schema_to_xml (added in #10239) carries a seen frozenset to stop at cycles — the resolver should do the same.

2. Refs not shaped like #/$defs/<name>KeyError

The resolver takes ref_path = obj["$ref"].split("/")[-1] and looks it up in schema["$defs"] only. Local pointers from OpenAPI-style or hand-written schemas crash tool construction:

from dspy.adapters.types.tool import convert_input_schema_to_tool_args

schema = {
    "type": "object",
    "$defs": {"Other": {"type": "string"}},
    "properties": {
        "node": {"$ref": "#/definitions/Node"},
        "y": {"type": "object", "properties": {"z": {"$ref": "#/properties/x"}}},
    },
    "required": ["node"],
    "definitions": {"Node": {"type": "object", "properties": {"name": {"type": "string"}}}},
}
convert_input_schema_to_tool_args(schema)  # KeyError: 'Node'

Both refs above point into document locations the JSON Schema pointer syntax addresses explicitly (#/definitions/..., #/properties/...), and neither resolves: the first KeyErrors (present in definitions, looked up in $defs), the second KeyErrors ('x' is not in $defs).

3. Legacy definitions-only schema → silent degradation

When the top-level schema has definitions but no $defs, convert_input_schema_to_tool_args never attempts resolution (defs = schema.get("$defs", {}) is empty), so the {"$ref": "#/definitions/Node"} dict is passed through verbatim as the argument schema and arg_types falls back to Any. The resulting tool advertises a dangling $ref to providers (rejected by strict-schema APIs) and loses all validation. The docstring/guard in _resolve_json_schema_reference checks "$defs" not in schema and "definitions" not in schema, so definitions was clearly intended to be supported — the lookup just never implements it.

Proposed fix (happy to send PR — I have it working locally)

  1. Cycle-safe expansion: track ref names on the current expansion stack; when a cycle closes, emit {} (the JSON-Schema "any" schema) instead of recursing — semantically the correct stop and universally accepted by providers. Depth is bounded by the ref graph, not the recursion limit.
  2. Pointer-aware lookup: resolve refs against $defs, definitions, and local document pointers (#/… paths from the root), not just the last segment of $defs. An unresolvable ref keeps its current pass-through behavior rather than raising KeyError.
  3. convert_input_schema_to_tool_args: seed defs from $defs or definitions so legacy schemas resolve instead of silently degrading.

All three are behavior-preserving for today's working paths (#/$defs/X refs from pydantic behave identically; only cycles and currently-crashing inputs change). Verified locally: recursive model tools construct fine, and the crash cases in #2/#3 return sensible schemas.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions