diff --git a/tests/server/fastmcp/test_func_metadata.py b/tests/server/fastmcp/test_func_metadata.py index 840509906..270d4800d 100644 --- a/tests/server/fastmcp/test_func_metadata.py +++ b/tests/server/fastmcp/test_func_metadata.py @@ -454,6 +454,98 @@ def func_with_str_and_int(a: str, b: int): assert result["b"] == 123 +def test_str_annotation_preserves_json_string(): + """ + Regression test for PR #1113: Ensure that when a parameter is annotated as str, + valid JSON strings are NOT parsed into Python objects. + + This test would fail before the fix (JSON string would be parsed to dict) + and passes after the fix (JSON string remains as string). + """ + + def process_json_config(config: str, enabled: bool = True) -> str: + """Function that expects a JSON string as a string parameter.""" + # In real use, this function might validate or transform the JSON string + # before parsing it, or pass it to another service as-is + return f"Processing config: {config}" + + meta = func_metadata(process_json_config) + + # Test case 1: JSON object as string + json_obj_str = '{"database": "postgres", "port": 5432}' + result = meta.pre_parse_json({"config": json_obj_str, "enabled": True}) + + # The config parameter should remain as a string, NOT be parsed to a dict + assert isinstance(result["config"], str) + assert result["config"] == json_obj_str + + # Test case 2: JSON array as string + json_array_str = '["item1", "item2", "item3"]' + result = meta.pre_parse_json({"config": json_array_str}) + + # Should remain as string + assert isinstance(result["config"], str) + assert result["config"] == json_array_str + + # Test case 3: JSON string value (double-encoded) + json_string_str = '"This is a JSON string"' + result = meta.pre_parse_json({"config": json_string_str}) + + # Should remain as the original string with quotes + assert isinstance(result["config"], str) + assert result["config"] == json_string_str + + # Test case 4: Complex nested JSON as string + complex_json_str = '{"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}], "count": 2}' + result = meta.pre_parse_json({"config": complex_json_str}) + + # Should remain as string + assert isinstance(result["config"], str) + assert result["config"] == complex_json_str + + +@pytest.mark.anyio +async def test_str_annotation_runtime_validation(): + """ + Regression test for PR #1113: Test runtime validation with string parameters + containing valid JSON to ensure they are passed as strings, not parsed objects. + """ + + def handle_json_payload(payload: str, strict_mode: bool = False) -> str: + """Function that processes a JSON payload as a string.""" + # This function expects to receive the raw JSON string + # It might parse it later after validation or logging + assert isinstance(payload, str), f"Expected str, got {type(payload)}" + return f"Handled payload of length {len(payload)}" + + meta = func_metadata(handle_json_payload) + + # Test with a JSON object string + json_payload = '{"action": "create", "resource": "user", "data": {"name": "Test User"}}' + + result = await meta.call_fn_with_arg_validation( + handle_json_payload, + fn_is_async=False, + arguments_to_validate={"payload": json_payload, "strict_mode": True}, + arguments_to_pass_directly=None, + ) + + # The function should have received the string and returned successfully + assert result == f"Handled payload of length {len(json_payload)}" + + # Test with JSON array string + json_array_payload = '["task1", "task2", "task3"]' + + result = await meta.call_fn_with_arg_validation( + handle_json_payload, + fn_is_async=False, + arguments_to_validate={"payload": json_array_payload}, + arguments_to_pass_directly=None, + ) + + assert result == f"Handled payload of length {len(json_array_payload)}" + + # Tests for structured output functionality