Skip to content

OpenAPI: rewrite $ref under propertyNames and patternProperties in _replace_ref_with_defs; add regression test for dict[StrEnum, Model]#3306

Merged
jlowin merged 6 commits intoPrefectHQ:mainfrom
manojPal23234:fix/normalize-propertynames-refs
Feb 26, 2026
Merged

OpenAPI: rewrite $ref under propertyNames and patternProperties in _replace_ref_with_defs; add regression test for dict[StrEnum, Model]#3306
jlowin merged 6 commits intoPrefectHQ:mainfrom
manojPal23234:fix/normalize-propertynames-refs

Conversation

@manojPal23234
Copy link
Copy Markdown
Contributor

@manojPal23234 manojPal23234 commented Feb 26, 2026

Summary

This PR fixes a gap in the OpenAPI → JSON Schema normalization path. The function _replace_ref_with_defs already rewrote #/components/schemas/... to #/$defs/... in common locations (e.g., properties, items, additionalProperties, and combinators), but it did not recurse into propertyNames (and patternProperties).
As a result, schemas like dict[StrEnum, Model] (where keys are a StrEnum) produced a dangling propertyNames.$ref pointing to #/components/schemas/..., which some MCP clients failed to resolve.

This change adds explicit handling for both propertyNames and patternProperties inside the established OpenAPI normalization chokepoint.

Fixes #3303.


What changed

  • src/fastmcp/utilities/openapi/schemas.py

    • In _replace_ref_with_defs(...), also recurse into:
      • propertyNames (mirrors the existing pattern used for additionalProperties)
      • patternProperties (recurses into each sub‑schema)
  • Tests (OpenAPI utilities)

    • Added a focused regression test that reproduces the dict[StrEnum, Model] shape and asserts:
      • additionalProperties.$ref#/$defs/ItemInfo
      • propertyNames.$ref#/$defs/Category
      • No remaining #/components/schemas/ in the result

    Test file path:
    tests\utilities\openapi\test_propertynames_ref_rewrite.py


Rationale & scope

  • Keeps the fix localized to the OpenAPI layer (_replace_ref_with_defs) per review feedback, avoiding a second, parallel normalization path in generic JSON‑Schema utilities.
  • No collision/aliasing logic is introduced; this path is not expected to receive schemas with conflicting "$defs" and components.schemas.
  • Minimal, keyword‑targeted diff that closes the specific bug without altering broader behavior.

Regression test (excerpt)

from fastmcp.utilities.openapi.schemas import _replace_ref_with_defs

def test_replace_ref_with_defs_rewrites_propertyNames_ref():
    """
    Regression test for dict[StrEnum, Model] case:
    propertyNames.$ref and additionalProperties.$ref must both normalize to #/$defs/...
    """
    schema = {
        "type": "object",
        "propertyNames": {"$ref": "#/components/schemas/Category"},
        "additionalProperties": {"$ref": "#/components/schemas/ItemInfo"},
    }

    result = _replace_ref_with_defs(schema)

    assert result["additionalProperties"]["$ref"] == "#/$defs/ItemInfo"
    assert result["propertyNames"]["$ref"] == "#/$defs/Category"
    assert "#/components/schemas/" not in str(result)

@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. openapi Related to OpenAPI integration, parsing, or code generation features. labels Feb 26, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/fastmcp/utilities/json_schema.py Outdated
if isinstance(comp_defs, dict) and comp_defs:
dst = schema.setdefault("$defs", {})
for k, v in comp_defs.items():
dst.setdefault(k, v)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 manojPal23234 changed the title Normalize $ref inside propertyNames (and everywhere); migrate components→$defs; add regression test Normalize $ref in propertyNames (and everywhere), deterministic components→$defs migration; add regression tests Feb 26, 2026
Copy link
Copy Markdown
Contributor Author

@manojPal23234 manojPal23234 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deterministic migration: replaced dst.setdefault(k, v) with a collision-aware policy.

  • If both $defs.Name and components.schemas.Name exist and differ:
    • Preserve the old $defs.Name only 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 $defs entries.

Copy link
Copy Markdown
Member

@jlowin jlowin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Add propertyNames and patternProperties handling to _replace_ref_with_defs (same pattern as additionalProperties)
  2. Add a regression test for the dict[StrEnum, Model] case from #3303, in the existing test suite under tests/utilities/openapi/

That should be a pretty small diff. Happy to help if you have questions about the codebase structure.

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Feb 26, 2026

Note: This comment was edited with updated analysis.

Test Failure Analysis

Summary: The CI static_analysis job failed because the PR was submitted without running prek (pre-commit hooks) locally first. This is a pure formatting/linting issue — no logic errors.

Root Cause: The ruff check and ruff format hooks found issues in 3 files:

  • Unused import pytest in tests/utilities/openapi/test_propertynames_ref_rewrite.py
  • Formatting issues in src/fastmcp/utilities/openapi/schemas.py (indentation of a closing })
  • Missing trailing newline in src/fastmcp/utilities/json_schema.py

Suggested Solution: Run prek locally and commit the resulting changes:

```bash
uv run prek run --all-files
git add -u
git commit -m "Fix formatting"
```

Detailed Analysis

Failed Step: Run prek

Error 1 — ruff check (1 auto-fix applied):
```
ruff check...............................................................[Failed]

  • hook id: ruff-check

  • exit code: 1

  • files were modified by this hook

    Found 1 error (1 fixed, 0 remaining).
    ```
    Removed unused import pytest from tests/utilities/openapi/test_propertynames_ref_rewrite.py.

Error 2 — ruff format (3 files reformatted):
```
ruff format..............................................................[Failed]

  • hook id: ruff-format

  • files were modified by this hook

    3 files reformatted, 622 files left unchanged
    ```

Diffs that need to be applied

src/fastmcp/utilities/json_schema.py — Add trailing newline:
```diff

  • return schema
    \ No newline at end of file
  • return schema
    ```

src/fastmcp/utilities/openapi/schemas.py — Fix indentation of closing brace:
```diff
schema["patternProperties"] = {
pattern: _replace_ref_with_defs(subschema)
for pattern, subschema in pattern_properties.items()

  •    }
    
  •        }
    

```

tests/utilities/openapi/test_propertynames_ref_rewrite.py — Remove unused import and reformat dict literals/assertions to single-line style.

Related Files
  • src/fastmcp/utilities/json_schema.py — missing trailing newline
  • src/fastmcp/utilities/openapi/schemas.py — indentation issue in patternProperties block
  • tests/utilities/openapi/test_propertynames_ref_rewrite.py — unused import pytest, formatting issues

@manojPal23234 manojPal23234 force-pushed the fix/normalize-propertynames-refs branch from ffbd7bf to d80f786 Compare February 26, 2026 17:33
@manojPal23234 manojPal23234 changed the title Normalize $ref in propertyNames (and everywhere), deterministic components→$defs migration; add regression tests **Title:** OpenAPI: rewrite $ref under propertyNames (and patternProperties) in _replace_ref_with_defs; add regression test for dict[StrEnum, Model] Feb 26, 2026
@manojPal23234 manojPal23234 changed the title **Title:** OpenAPI: rewrite $ref under propertyNames (and patternProperties) in _replace_ref_with_defs; add regression test for dict[StrEnum, Model] OpenAPI: rewrite $ref under propertyNames and patternProperties in _replace_ref_with_defs; add regression test for dict[StrEnum, Model] Feb 26, 2026
@manojPal23234
Copy link
Copy Markdown
Contributor Author

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:

  1. Add propertyNames and patternProperties handling to _replace_ref_with_defs (same pattern as additionalProperties)
  2. Add a regression test for the dict[StrEnum, Model] case from propertyNames.$ref not converted by _replace_ref_with_defs #3303, in the existing test suite under tests/utilities/openapi/

That should be a pretty small diff. Happy to help if you have questions about the codebase structure.

Hi! Thanks for the guidance and for clarifying the right layer to fix this in.
I’ve updated the PR to:

Scope the normalization to the OpenAPI chokepoint (_replace_ref_with_defs) as suggested.
Add recursive handling for:

propertyNames (mirrors existing additionalProperties handling), and
patternProperties (recurses into each sub‑schema).

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.

@manojPal23234 manojPal23234 requested a review from jlowin February 26, 2026 18:00
Copy link
Copy Markdown
Member

@jlowin jlowin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! A couple small static failures I'll clean up.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@jlowin jlowin merged commit 507e6b8 into PrefectHQ:main Feb 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. openapi Related to OpenAPI integration, parsing, or code generation features.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

propertyNames.$ref not converted by _replace_ref_with_defs

2 participants