Skip to content

Commit 923695b

Browse files
authored
fix: strip discriminator after dereferencing schemas (#3682)
1 parent 5338629 commit 923695b

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

src/fastmcp/utilities/json_schema.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,33 @@ def _strip_remote_refs(obj: Any) -> Any:
7373
return obj
7474

7575

76+
def _strip_discriminator(obj: Any) -> Any:
77+
"""Recursively remove OpenAPI ``discriminator`` keys from a schema.
78+
79+
Pydantic emits ``discriminator.mapping`` with values like
80+
``#/$defs/ClassName``. After ``$defs`` are inlined and removed by
81+
``dereference_refs``, those mapping entries dangle. The keyword is an
82+
OpenAPI extension — the ``anyOf`` variants already carry ``const`` on
83+
the discriminant field, so the mapping is redundant.
84+
85+
Only strips ``discriminator`` when it appears alongside ``anyOf`` or
86+
``oneOf``, which is where the OpenAPI keyword lives. A property
87+
*named* ``discriminator`` (inside ``properties``) is left alone.
88+
"""
89+
if isinstance(obj, dict):
90+
skip = "discriminator" in obj and ("anyOf" in obj or "oneOf" in obj)
91+
# Keys that hold instance data, not sub-schemas — don't recurse.
92+
_DATA_KEYS = {"default", "const", "examples", "enum"}
93+
return {
94+
k: (v if k in _DATA_KEYS else _strip_discriminator(v))
95+
for k, v in obj.items()
96+
if not (k == "discriminator" and skip)
97+
}
98+
if isinstance(obj, list):
99+
return [_strip_discriminator(item) for item in obj]
100+
return obj
101+
102+
76103
def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]:
77104
"""Resolve all $ref references in a JSON schema by inlining definitions.
78105
@@ -135,6 +162,13 @@ def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]:
135162
if "$defs" in dereferenced:
136163
dereferenced = {k: v for k, v in dereferenced.items() if k != "$defs"}
137164

165+
# Strip `discriminator` keys — they contain `mapping` values that
166+
# point at `#/$defs/...` entries we just removed. `discriminator`
167+
# is an OpenAPI extension; after inlining, the `anyOf` variants
168+
# already carry `const` on the discriminant field, making the
169+
# mapping redundant.
170+
dereferenced = _strip_discriminator(dereferenced)
171+
138172
return dereferenced
139173

140174
except JsonRefError:

tests/utilities/test_json_schema.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,67 @@ def test_preserves_nested_siblings(self):
197197
assert country["default"] == "US"
198198
assert "$defs" not in result
199199

200+
def test_strips_discriminator_mapping_after_inlining(self):
201+
"""Discriminator.mapping refs dangle after $defs are inlined (#3679)."""
202+
schema = {
203+
"$defs": {
204+
"IdentifyPerson": {
205+
"type": "object",
206+
"properties": {
207+
"action": {"const": "identify", "type": "string"},
208+
"name": {"type": "string"},
209+
},
210+
"required": ["action", "name"],
211+
},
212+
"PersonDelete": {
213+
"type": "object",
214+
"properties": {
215+
"action": {"const": "delete", "type": "string"},
216+
},
217+
"required": ["action"],
218+
},
219+
},
220+
"anyOf": [
221+
{"$ref": "#/$defs/IdentifyPerson"},
222+
{"$ref": "#/$defs/PersonDelete"},
223+
],
224+
"discriminator": {
225+
"mapping": {
226+
"identify": "#/$defs/IdentifyPerson",
227+
"delete": "#/$defs/PersonDelete",
228+
},
229+
"propertyName": "action",
230+
},
231+
}
232+
result = dereference_refs(schema)
233+
234+
assert "$defs" not in result
235+
assert "discriminator" not in result
236+
# The anyOf variants should be inlined with their const values intact
237+
assert len(result["anyOf"]) == 2
238+
actions = {v["properties"]["action"]["const"] for v in result["anyOf"]}
239+
assert actions == {"identify", "delete"}
240+
241+
def test_preserves_property_named_discriminator(self):
242+
"""A field *named* 'discriminator' inside properties must survive."""
243+
schema = {
244+
"$defs": {
245+
"Inner": {
246+
"type": "object",
247+
"properties": {
248+
"discriminator": {"type": "string"},
249+
},
250+
},
251+
},
252+
"properties": {
253+
"item": {"$ref": "#/$defs/Inner"},
254+
},
255+
}
256+
result = dereference_refs(schema)
257+
258+
assert "$defs" not in result
259+
assert "discriminator" in result["properties"]["item"]["properties"]
260+
200261

201262
class TestCompressSchema:
202263
"""Tests for the compress_schema function."""

0 commit comments

Comments
 (0)