OpenAPI: rewrite $ref under propertyNames and patternProperties in _replace_ref_with_defs; add regression test for dict[StrEnum, Model]#3306
Conversation
…onents→$defs; add regression test
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 653508ba56
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if isinstance(comp_defs, dict) and comp_defs: | ||
| dst = schema.setdefault("$defs", {}) | ||
| for k, v in comp_defs.items(): | ||
| dst.setdefault(k, v) |
There was a problem hiding this comment.
Overwrite colliding $defs entries when migrating components
Using dst.setdefault(k, v) keeps any pre-existing $defs entry even after all #/components/schemas/... references are rewritten to #/$defs/..., so mixed schemas can dereference to the wrong definition. In a schema that already has $defs.User and also has components.schemas.User, every rewritten ref now points at the stale $defs.User instead of the component schema, which changes validation semantics after this commit. Please make migration deterministic for collisions (e.g., prefer components.schemas when normalizing component refs).
Useful? React with 👍 / 👎.
…on; preserve direct $defs refs via alias; add collision test
manojPal23234
left a comment
There was a problem hiding this comment.
Deterministic migration: replaced dst.setdefault(k, v) with a collision-aware policy.
- If both
$defs.Nameandcomponents.schemas.Nameexist and differ:- Preserve the old
$defs.Nameonly if it’s referenced directly via#/$defs/Name, by moving it to an alias (e.g.,Name__preexisting) and rewriting those direct refs. - Then overwrite
$defs[Name]with the component definition so normalized component refs resolve to the intended schema.
This removes migration order dependence and avoids mixed schemas silently picking stale$defsentries.
- Preserve the old
jlowin
left a comment
There was a problem hiding this comment.
Thanks for digging into this — the issue is real and your investigation of the collision edge case shows careful thinking about the problem space.
That said, I think the fix belongs in a different layer. The function _replace_ref_with_defs in src/fastmcp/utilities/openapi/schemas.py is the established chokepoint for converting OpenAPI-style #/components/schemas/ refs into JSON Schema #/$defs/ refs. It already handles properties, items, additionalProperties, and combinators — it just doesn't recurse into propertyNames (or patternProperties, which has the same shape of problem).
This is essentially a one-line fix in _replace_ref_with_defs — add propertyNames handling the same way additionalProperties is handled. Adding a second, parallel normalization path in json_schema.py (which handles generic JSON Schema and shouldn't need to know about OpenAPI conventions) creates two different rewriting strategies that don't know about each other, which is a maintenance problem.
The collision handling (_migrate_components_to_defs with aliasing) isn't something we need — schemas coming through this path won't have both $defs and components.schemas with conflicting definitions. Let's keep this scoped to the actual bug.
So the ideal version of this PR would be:
- Add
propertyNamesandpatternPropertieshandling to_replace_ref_with_defs(same pattern asadditionalProperties) - Add a regression test for the
dict[StrEnum, Model]case from #3303, in the existing test suite undertests/utilities/openapi/
That should be a pretty small diff. Happy to help if you have questions about the codebase structure.
Test Failure AnalysisSummary: The CI Root Cause: The
Suggested Solution: Run ```bash Detailed AnalysisFailed Step:
|
ffbd7bf to
d80f786
Compare
$ref under propertyNames (and patternProperties) in _replace_ref_with_defs; add regression test for dict[StrEnum, Model]
$ref under propertyNames (and patternProperties) in _replace_ref_with_defs; add regression test for dict[StrEnum, Model]
Hi! Thanks for the guidance and for clarifying the right layer to fix this in. Scope the normalization to the OpenAPI chokepoint (_replace_ref_with_defs) as suggested. propertyNames (mirrors existing additionalProperties handling), and Include a regression test for the dict[StrEnum, Model] case (keys via propertyNames, values via additionalProperties) that verifies both refs are rewritten to #/$defs/... and no #/components/schemas/... remains. Per your review, I removed the earlier generic normalization and the collision/aliasing logic from json_schema.py to avoid duplicating strategies and to keep this fix focused on the actual gap. |
jlowin
left a comment
There was a problem hiding this comment.
Thanks! A couple small static failures I'll clean up.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e640700a7b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if pattern_properties := schema.get("patternProperties"): | ||
| if isinstance(pattern_properties, dict): | ||
| schema["patternProperties"] = { | ||
| pattern: _replace_ref_with_defs(subschema) |
There was a problem hiding this comment.
Skip boolean subschemas in patternProperties
The new patternProperties rewrite path unconditionally calls _replace_ref_with_defs(subschema) for each value, but this helper expects a dict and starts with info.copy(). In JSON Schema/OpenAPI 3.1, boolean schemas are valid in patternProperties (for example "^x$": false), so this now raises an AttributeError and aborts schema normalization for otherwise valid specs. Please mirror the additionalProperties handling by bypassing recursion for non-dict entries.
Useful? React with 👍 / 👎.
Summary
This PR fixes a gap in the OpenAPI → JSON Schema normalization path. The function
_replace_ref_with_defsalready rewrote#/components/schemas/...to#/$defs/...in common locations (e.g., properties, items,additionalProperties, and combinators), but it did not recurse intopropertyNames(andpatternProperties).As a result, schemas like
dict[StrEnum, Model](where keys are aStrEnum) produced a danglingpropertyNames.$refpointing to#/components/schemas/..., which some MCP clients failed to resolve.This change adds explicit handling for both
propertyNamesandpatternPropertiesinside the established OpenAPI normalization chokepoint.Fixes #3303.
What changed
src/fastmcp/utilities/openapi/schemas.py_replace_ref_with_defs(...), also recurse into:propertyNames(mirrors the existing pattern used foradditionalProperties)patternProperties(recurses into each sub‑schema)Tests (OpenAPI utilities)
dict[StrEnum, Model]shape and asserts:additionalProperties.$ref→#/$defs/ItemInfopropertyNames.$ref→#/$defs/Category#/components/schemas/in the resultTest file path:
tests\utilities\openapi\test_propertynames_ref_rewrite.pyRationale & scope
_replace_ref_with_defs) per review feedback, avoiding a second, parallel normalization path in generic JSON‑Schema utilities."$defs"andcomponents.schemas.Regression test (excerpt)