Skip to content

Fix flattening of query/header/cookie param models with sibling params#15130

Open
maksimzayats wants to merge 6 commits intofastapi:masterfrom
maksimzayats:handle-query-siblings
Open

Fix flattening of query/header/cookie param models with sibling params#15130
maksimzayats wants to merge 6 commits intofastapi:masterfrom
maksimzayats:handle-query-siblings

Conversation

@maksimzayats
Copy link
Copy Markdown

@maksimzayats maksimzayats commented Mar 16, 2026

Summary

Fix Pydantic query/header/cookie models so they still flatten when there are sibling params in the same location.

Problem

FastAPI only flattened these models when they were the only param there:

  • Annotated[MyModel, Query()]
  • Annotated[MyModel, Header()]
  • Annotated[MyModel, Cookie()]

If you added a sibling param like extra: Query() or extra: Header(), the model stopped flattening. That broke request parsing, validation errors, and OpenAPI docs.

Fix

Model-backed query/header/cookie params now flatten consistently, even with sibling params.

This keeps existing behavior for the single-model case and preserves:

  • aliases / validation aliases
  • list handling
  • header underscore conversion
  • body embedding behavior

Old vs New

Before:

def read_items(
    m: Annotated[Filters, Query()],
    extra: Annotated[int, Query()],
)

FastAPI could treat m like one query param instead of flattening it into limit and q.

After:

  • limit, q, and extra are all handled as separate query params
  • validation errors point to the individual fields
  • OpenAPI shows flattened params, not m

Same fix applies to Header() and Cookie().

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 16, 2026

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing maksimzayats:handle-query-siblings (c1bc565) with master (07dfa9a)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (a28b785) during the generation of this report, so 07dfa9a was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@maksimzayats
Copy link
Copy Markdown
Author

Hi folks! Could somebody please take a look?
It's currently blocking our use cases a bit...

@maksimzayats
Copy link
Copy Markdown
Author

Hi @tiangolo @svlandeg ! Just a kind ping :)
Could you please take a look on this or re-ping somebody else?

@svlandeg
Copy link
Copy Markdown
Member

Hi @maksimzayats, this seems to be a duplicate of #12481. Could you checkout that branch and report back whether it solves things for your use-case?

@nidhishgajjar
Copy link
Copy Markdown

Orb Code Review (powered by GLM 5.1 on Orb Cloud)

Review of PR #15130: Fix flattening of query/header/cookie param models with sibling params

This is a significant enhancement that removes a long-standing limitation where Pydantic parameter models (e.g., Annotated[QueryModel, Query()]) were only flattened when used alone. Now they flatten correctly even alongside sibling parameters.

What looks good ✅

  • _FlatParamField dataclass: Clean abstraction for tracking both the flattened sub-field and its original parent model — essential for correct validation grouping
  • Comprehensive test coverage: 288 lines covering query, header, and cookie models in isolation, mixed with siblings, with OpenAPI metadata, convert_underscores, include_in_schema, etc.
  • Backward compatible: Existing single-model behavior is preserved
  • OpenAPI schema correctness: Flattened fields appear correctly in the schema, including metadata like descriptions, examples, and deprecation

Concerns 🟡

  1. Duplicate code in OpenAPI utils: The header parameter processing in _get_openapi_operation_parameters() now has a near-duplicate of the parameter-building loop. Consider extracting a helper like _build_openapi_parameter() to reduce duplication (~30 lines duplicated).

  2. Commented-out code: There's a commented line # field_info = cast(Param, field_info) — should this be removed or is it a TODO?

  3. Edge case — name collisions: What happens if a model field name matches a sibling param name? E.g.:

    def endpoint(
        m: Annotated[QueryModel, Query()],  # has field "limit"
        limit: Annotated[int, Query()],      # same name!
    ):

    This isn't covered by tests and could lead to ambiguous behavior.

  4. processed_extras goes to one model: When len(model_fields) == 1, extra params get merged into that model's dict. With multiple models, extras are separate. This seems intentional but worth documenting.

Summary

Solid feature enhancement with excellent test coverage. The main improvement opportunity is reducing the duplicated OpenAPI parameter-building code. The name collision edge case should be considered, even if the current behavior is documented as undefined.

Assessment: comment (code duplication, edge cases worth discussing)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants