Magnus Jeremiah Lowin commited on
Commit
dd2c763
·
unverified ·
1 Parent(s): b2c5766

fix: replace oneOf with anyOf in OpenAPI schemas to handle overlapping unions (#1119)

Browse files
src/fastmcp/utilities/openapi.py CHANGED
@@ -1,6 +1,6 @@
1
  import json
2
  import logging
3
- from typing import Any, Generic, Literal, TypeVar
4
 
5
  from openapi_pydantic import (
6
  OpenAPI,
@@ -1188,6 +1188,20 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1188
  return result
1189
 
1190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1191
  def extract_output_schema_from_responses(
1192
  responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None
1193
  ) -> dict[str, Any] | None:
@@ -1276,4 +1290,7 @@ def extract_output_schema_from_responses(
1276
  # Use compress_schema to remove unused definitions
1277
  output_schema = compress_schema(output_schema)
1278
 
 
 
 
1279
  return output_schema
 
1
  import json
2
  import logging
3
+ from typing import Any, Generic, Literal, TypeVar, cast
4
 
5
  from openapi_pydantic import (
6
  OpenAPI,
 
1188
  return result
1189
 
1190
 
1191
+ def _adjust_union_types(
1192
+ schema: dict[str, Any] | list[Any],
1193
+ ) -> dict[str, Any] | list[Any]:
1194
+ """Recursively replace 'oneOf' with 'anyOf' in schema to handle overlapping unions."""
1195
+ if isinstance(schema, dict):
1196
+ if "oneOf" in schema:
1197
+ schema["anyOf"] = schema.pop("oneOf")
1198
+ for k, v in schema.items():
1199
+ schema[k] = _adjust_union_types(v)
1200
+ elif isinstance(schema, list):
1201
+ return [_adjust_union_types(item) for item in schema]
1202
+ return schema
1203
+
1204
+
1205
  def extract_output_schema_from_responses(
1206
  responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None
1207
  ) -> dict[str, Any] | None:
 
1290
  # Use compress_schema to remove unused definitions
1291
  output_schema = compress_schema(output_schema)
1292
 
1293
+ # Adjust union types to handle overlapping unions
1294
+ output_schema = cast(dict[str, Any], _adjust_union_types(output_schema))
1295
+
1296
  return output_schema
tests/utilities/openapi/test_openapi_output_schemas.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  from fastmcp.utilities.openapi import (
4
  ResponseInfo,
 
5
  extract_output_schema_from_responses,
6
  )
7
 
@@ -234,3 +235,42 @@ class TestExtractOutputSchema:
234
  assert "User" in result["$defs"]
235
  assert result["properties"]["result"]["type"] == "array"
236
  assert result["properties"]["result"]["items"]["$ref"] == "#/$defs/User"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  from fastmcp.utilities.openapi import (
4
  ResponseInfo,
5
+ _adjust_union_types,
6
  extract_output_schema_from_responses,
7
  )
8
 
 
235
  assert "User" in result["$defs"]
236
  assert result["properties"]["result"]["type"] == "array"
237
  assert result["properties"]["result"]["items"]["$ref"] == "#/$defs/User"
238
+
239
+
240
+ def test_adjust_union_types():
241
+ """Test that oneOf is replaced with anyOf in schemas."""
242
+ schema = {"oneOf": [{"type": "string"}, {"type": "number"}]}
243
+ result = _adjust_union_types(schema)
244
+ assert isinstance(result, dict)
245
+ assert "anyOf" in result
246
+ assert "oneOf" not in result
247
+ assert len(result["anyOf"]) == 2
248
+ assert result["anyOf"][0] == {"type": "string"}
249
+ assert result["anyOf"][1] == {"type": "number"}
250
+
251
+
252
+ def test_extract_output_schema_converts_oneOf_to_anyOf():
253
+ """Test that extracted schema converts oneOf to anyOf."""
254
+ responses = {
255
+ "200": ResponseInfo(
256
+ description="Success",
257
+ content_schema={
258
+ "application/json": {
259
+ "type": "object",
260
+ "properties": {
261
+ "result": {
262
+ "oneOf": [
263
+ {"$ref": "#/$defs/TypeA"},
264
+ {"$ref": "#/$defs/TypeB"},
265
+ ]
266
+ }
267
+ },
268
+ }
269
+ },
270
+ )
271
+ }
272
+ schema_definitions = {"TypeA": {"type": "string"}, "TypeB": {"type": "number"}}
273
+ result = extract_output_schema_from_responses(responses, schema_definitions)
274
+ assert result is not None
275
+ assert "oneOf" not in str(result) # Ensure no oneOf remains
276
+ assert "anyOf" in str(result) # Ensure anyOf is present