fix: prevent schema mutation in _prune_param and _convert_nullable_field (#3927)

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bill Easton 2026-04-14 11:10:27 -05:00 committed by GitHub
commit 8d1b28958f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 27 additions and 1 deletions

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import copy
from collections import defaultdict
from typing import Any
@ -287,6 +288,7 @@ def _prune_param(schema: dict[str, Any], param: str) -> dict[str, Any]:
"""Return a new schema with *param* removed from `properties`, `required`,
and (if no longer referenced) `$defs`.
"""
schema = copy.deepcopy(schema)
# ── 1. drop from properties/required ──────────────────────────────
props = schema.get("properties", {})

View file

@ -173,7 +173,7 @@ def _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]:
elif "anyOf" in result:
# Add null to anyOf if not present
if not any(item.get("type") == "null" for item in result["anyOf"]):
result["anyOf"].append({"type": "null"})
result["anyOf"] = [*result["anyOf"], {"type": "null"}]
elif "allOf" in result:
# Wrap allOf in anyOf with null option
result["anyOf"] = [{"allOf": result.pop("allOf")}, {"type": "null"}]

View file

@ -8,6 +8,7 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.providers.openapi import OpenAPIProvider
from fastmcp.utilities.openapi.json_schema_converter import (
_convert_nullable_field,
convert_openapi_schema_to_json_schema,
)
@ -556,3 +557,14 @@ class TestNullableInputSchemaIntegration:
assert "nullable" not in bio_prop
assert bio_prop["type"] == ["string", "null"]
class TestConvertNullableFieldMutation:
"""Test that _convert_nullable_field does not mutate its input."""
def test_does_not_mutate_anyof_list(self):
"""_convert_nullable_field should not append to the original anyOf list."""
schema = {"anyOf": [{"type": "string"}], "nullable": True}
original_len = len(schema["anyOf"])
_convert_nullable_field(schema)
assert len(schema["anyOf"]) == original_len

View file

@ -1,3 +1,4 @@
import copy
from unittest.mock import patch
from jsonref import replace_refs
@ -51,6 +52,17 @@ class TestPruneParam:
result = _prune_param(schema, "foo")
assert "required" not in result
def test_does_not_mutate_input(self):
"""Test that _prune_param does not mutate the original schema."""
schema = {
"type": "object",
"properties": {"a": {"type": "string"}, "b": {"type": "integer"}},
"required": ["a", "b"],
}
original = copy.deepcopy(schema)
_prune_param(schema, "a")
assert schema == original
class TestDereferenceRefs:
"""Tests for the dereference_refs function."""