Fix optional parameter validation in OpenAPI integration (#1135)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jeremiah Lowin 2025-07-13 10:23:56 -04:00 committed by GitHub
commit 7ce0a59ea6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 142 additions and 1 deletions

View file

@ -1095,6 +1095,35 @@ def _replace_ref_with_defs(
return schema
def _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]:
"""
Make an optional parameter schema nullable to allow None values.
For optional parameters, we need to allow null values in addition to the
specified type to handle cases where None is passed for optional parameters.
"""
# If schema already has multiple types or is already nullable, don't modify
if "anyOf" in schema or "oneOf" in schema or "allOf" in schema:
return schema
# If it's already nullable (type includes null), don't modify
if isinstance(schema.get("type"), list) and "null" in schema["type"]:
return schema
# Create a new schema that allows null in addition to the original type
if "type" in schema:
original_type = schema["type"]
if isinstance(original_type, str):
# Single type - make it a union with null
nullable_schema = schema.copy()
nullable_schema["anyOf"] = [{"type": original_type}, {"type": "null"}]
# Remove the original type since we're using anyOf
del nullable_schema["type"]
return nullable_schema
return schema
def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
"""
Combines parameter and request body schemas into a single schema.
@ -1156,15 +1185,25 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
else:
param_schema["description"] = location_desc
# Make optional parameters nullable to allow None values
if not param.required:
param_schema = _make_optional_parameter_nullable(param_schema)
properties[suffixed_name] = param_schema
else:
# No collision, use original name
if param.required:
required.append(param.name)
properties[param.name] = _replace_ref_with_defs(
param_schema = _replace_ref_with_defs(
param.schema_.copy(), param.description
)
# Make optional parameters nullable to allow None values
if not param.required:
param_schema = _make_optional_parameter_nullable(param_schema)
properties[param.name] = param_schema
# Add request body properties (no suffixes for body parameters)
if route.request_body and route.request_body.content_schema:
for prop_name, prop_schema in body_props.items():

View file

@ -0,0 +1,102 @@
"""Test for optional parameter handling in FastMCP OpenAPI integration."""
import pytest
from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo, _combine_schemas
async def test_optional_parameter_schema_allows_null():
"""Test that optional parameters generate schemas that allow null values."""
# Create a minimal HTTPRoute with optional parameter
optional_param = ParameterInfo(
name="optional_param",
location="query",
required=False,
schema={"type": "string"},
description="Optional parameter",
)
required_param = ParameterInfo(
name="required_param",
location="query",
required=True,
schema={"type": "string"},
description="Required parameter",
)
route = HTTPRoute(
method="GET",
path="/test",
parameters=[required_param, optional_param],
request_body=None,
responses={},
summary="Test endpoint",
description=None,
schema_definitions={},
)
# Generate combined schema
schema = _combine_schemas(route)
# Verify that optional parameter allows null values
optional_param_schema = schema["properties"]["optional_param"]
# Should have anyOf with string and null types
assert "anyOf" in optional_param_schema
assert {"type": "string"} in optional_param_schema["anyOf"]
assert {"type": "null"} in optional_param_schema["anyOf"]
# Required parameter should not allow null
required_param_schema = schema["properties"]["required_param"]
assert required_param_schema["type"] == "string"
assert "anyOf" not in required_param_schema
# Required list should only contain required param
assert "required_param" in schema["required"]
assert "optional_param" not in schema["required"]
@pytest.mark.parametrize(
"param_schema",
[
{"type": "string"},
{"type": "integer"},
{"type": "number"},
{"type": "boolean"},
{"type": "array", "items": {"type": "string"}},
{"type": "object", "properties": {"name": {"type": "string"}}},
],
)
async def test_optional_parameter_allows_null_for_type(param_schema):
"""Test that optional parameters of any type allow null values."""
optional_param = ParameterInfo(
name="optional_param",
location="query",
required=False,
schema=param_schema,
description="Optional parameter",
)
route = HTTPRoute(
method="GET",
path="/test",
parameters=[optional_param],
request_body=None,
responses={},
summary="Test endpoint",
description=None,
schema_definitions={},
)
# Generate combined schema
schema = _combine_schemas(route)
optional_param_schema = schema["properties"]["optional_param"]
# Should have anyOf with the original type and null
assert "anyOf" in optional_param_schema
assert {"type": "null"} in optional_param_schema["anyOf"]
# Check that original schema is preserved (either simple type or complex schema)
if "type" in param_schema:
assert {"type": param_schema["type"]} in optional_param_schema["anyOf"]
else:
assert param_schema in optional_param_schema["anyOf"]