mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
Fix optional parameter validation in OpenAPI integration (#1135)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c0b01ceb1f
commit
7ce0a59ea6
2 changed files with 142 additions and 1 deletions
|
|
@ -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():
|
||||
|
|
|
|||
102
tests/server/openapi/test_optional_parameters.py
Normal file
102
tests/server/openapi/test_optional_parameters.py
Normal 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"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue