From 507e6b80abc155dc8b824f0be9bda34a26ba558a Mon Sep 17 00:00:00 2001 From: manojPal23234 Date: Fri, 27 Feb 2026 02:38:54 +0530 Subject: [PATCH] OpenAPI: rewrite $ref under propertyNames and patternProperties in _replace_ref_with_defs; add regression test for dict[StrEnum, Model] (#3306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Normalize OpenAPI $ref everywhere (incl. propertyNames); migrate components→$defs; add regression test * Fix: normalize $ref in propertyNames and additionalProperties; add regression test * Deterministic migration: components.schemas override $defs on collision; preserve direct $defs refs via alias; add collision test * Fix: rewrite $ref in propertyNames and patternProperties in _replace_ref_with_defs * Fix syntax error, formatting, and stray files * Skip boolean subschemas in patternProperties --------- Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- src/fastmcp/utilities/openapi/schemas.py | 13 +++++++ .../openapi/test_propertynames_ref_rewrite.py | 34 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/utilities/openapi/test_propertynames_ref_rewrite.py diff --git a/src/fastmcp/utilities/openapi/schemas.py b/src/fastmcp/utilities/openapi/schemas.py index bb057f2eb..fa93b6c8d 100644 --- a/src/fastmcp/utilities/openapi/schemas.py +++ b/src/fastmcp/utilities/openapi/schemas.py @@ -123,6 +123,19 @@ def _replace_ref_with_defs( schema["additionalProperties"] = _replace_ref_with_defs( additionalProperties ) + # Handle propertyNames + if property_names := schema.get("propertyNames"): + if isinstance(property_names, dict): + schema["propertyNames"] = _replace_ref_with_defs(property_names) + # Handle patternProperties + if pattern_properties := schema.get("patternProperties"): + if isinstance(pattern_properties, dict): + schema["patternProperties"] = { + pattern: _replace_ref_with_defs(subschema) + if isinstance(subschema, dict) + else subschema + for pattern, subschema in pattern_properties.items() + } if info.get("description", description) and not schema.get("description"): schema["description"] = description return schema diff --git a/tests/utilities/openapi/test_propertynames_ref_rewrite.py b/tests/utilities/openapi/test_propertynames_ref_rewrite.py new file mode 100644 index 000000000..809363410 --- /dev/null +++ b/tests/utilities/openapi/test_propertynames_ref_rewrite.py @@ -0,0 +1,34 @@ +from fastmcp.utilities.openapi.schemas import _replace_ref_with_defs + + +def test_replace_ref_with_defs_rewrites_propertyNames_ref(): + """ + Regression test for issue #3303. + + When using dict[StrEnum, Model], Pydantic generates: + + { + "type": "object", + "propertyNames": {"$ref": "#/components/schemas/Category"}, + "additionalProperties": {"$ref": "#/components/schemas/ItemInfo"} + } + + _replace_ref_with_defs should rewrite BOTH refs to #/$defs/. + """ + + schema = { + "type": "object", + "propertyNames": {"$ref": "#/components/schemas/Category"}, + "additionalProperties": {"$ref": "#/components/schemas/ItemInfo"}, + } + + result = _replace_ref_with_defs(schema) + + # additionalProperties ref is rewritten + assert result["additionalProperties"]["$ref"] == "#/$defs/ItemInfo" + + # propertyNames ref must also be rewritten (this was the bug) + assert result["propertyNames"]["$ref"] == "#/$defs/Category" + + # Ensure no dangling OpenAPI refs remain + assert "#/components/schemas/" not in str(result)