fastapi - 💡(How to fix) Fix Bug with complex fields (`list[str]`) inside `BaseModel` [2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
fastapi/fastapi#14888Fetched 2026-04-08 00:21:41
View on GitHub
Comments
2
Participants
2
Timeline
10
Reactions
10
Author
Timeline (top)
mentioned ×3subscribed ×3commented ×2converted_to_discussion ×1

Example when "complex" field is moved from BaseModel to a separate attribute and gets parsed.

Code

from typing import Annotated
import uvicorn
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel

class MyParams(BaseModel):
    size: Annotated[int | None, Query()] = None

app = FastAPI()

@app.get("/items")
async def get_items(
    params: Annotated[MyParams, Depends()],
    field: Annotated[list[str] | None, Query()] = None,
) -> None:
    print("params: ", params)
    print("field: ", field)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

Request:

curl --location 'localhost:8000/items?size=100&field=foo&field=bar'

Expected:

params:  size=100
field:  ['foo', 'bar']
INFO:     127.0.0.1:55020 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

Actual:

params:  size=100
field:  ['foo', 'bar']
INFO:     127.0.0.1:55020 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

Root Cause

Example when "complex" field is moved from BaseModel to a separate attribute and gets parsed.

Code

from typing import Annotated
import uvicorn
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel

class MyParams(BaseModel):
    size: Annotated[int | None, Query()] = None

app = FastAPI()

@app.get("/items")
async def get_items(
    params: Annotated[MyParams, Depends()],
    field: Annotated[list[str] | None, Query()] = None,
) -> None:
    print("params: ", params)
    print("field: ", field)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

Request:

curl --location 'localhost:8000/items?size=100&field=foo&field=bar'

Expected:

params:  size=100
field:  ['foo', 'bar']
INFO:     127.0.0.1:55020 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

Actual:

params:  size=100
field:  ['foo', 'bar']
INFO:     127.0.0.1:55020 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

Code Example

from typing import Annotated
import uvicorn
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel

class MyParams(BaseModel):
    size: Annotated[int | None, Query()] = None
    field: Annotated[list[str] | None, Query()] = None

app = FastAPI()

@app.get("/items")
async def get_items(
    params: Annotated[MyParams, Depends()],
) -> None:
    print("params: ", params)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

---

curl --location 'localhost:8000/items?size=100&field=foo&field=bar'

---

params:  size=100 field=foo,bar
INFO:     127.0.0.1:54972 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

---

params:  size=100 field=None
INFO:     127.0.0.1:54972 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

---

from typing import Annotated
import uvicorn
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel

class MyParams(BaseModel):
    size: Annotated[int | None, Query()] = None

app = FastAPI()

@app.get("/items")
async def get_items(
    params: Annotated[MyParams, Depends()],
    field: Annotated[list[str] | None, Query()] = None,
) -> None:
    print("params: ", params)
    print("field: ", field)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

---

curl --location 'localhost:8000/items?size=100&field=foo&field=bar'

---

params:  size=100
field:  ['foo', 'bar']
INFO:     127.0.0.1:55020 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

---

params:  size=100
field:  ['foo', 'bar']
INFO:     127.0.0.1:55020 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

---

├── fastapi v0.128.7
│   ├── annotated-doc v0.0.4
│   ├── pydantic v2.12.5
│   │   ├── annotated-types v0.7.0
│   │   ├── pydantic-core v2.41.5
│   │   │   └── typing-extensions v4.15.0
│   │   ├── typing-extensions v4.15.0
│   │   ├── typing-inspection v0.4.2
│   │   │   └── typing-extensions v4.15.0
│   │   └── email-validator v2.3.0 (extra: email)
│   │       ├── dnspython v2.8.0
│   │       └── idna v3.11
│   ├── starlette v0.52.1
│   │   ├── anyio v4.12.1 (*)
│   │   └── typing-extensions v4.15.0
│   ├── typing-extensions v4.15.0
│   └── typing-inspection v0.4.2 (*)
RAW_BUFFERClick to expand / collapse

Privileged issue

  • I'm @tiangolo or he asked me directly to create an issue here.

Issue Content

Summary

When "complex" (e.g., list[str]) field is definied inside BaseModel, it doesn't get parsed.

MRE1

Summary

Example when "complex" field defined in BaseModel doesn't get parsed.

Code

from typing import Annotated
import uvicorn
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel

class MyParams(BaseModel):
    size: Annotated[int | None, Query()] = None
    field: Annotated[list[str] | None, Query()] = None

app = FastAPI()

@app.get("/items")
async def get_items(
    params: Annotated[MyParams, Depends()],
) -> None:
    print("params: ", params)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

Request:

curl --location 'localhost:8000/items?size=100&field=foo&field=bar'

Expected:

params:  size=100 field=foo,bar
INFO:     127.0.0.1:54972 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

Actual:

params:  size=100 field=None
INFO:     127.0.0.1:54972 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

MRE2

Summary

Example when "complex" field is moved from BaseModel to a separate attribute and gets parsed.

Code

from typing import Annotated
import uvicorn
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel

class MyParams(BaseModel):
    size: Annotated[int | None, Query()] = None

app = FastAPI()

@app.get("/items")
async def get_items(
    params: Annotated[MyParams, Depends()],
    field: Annotated[list[str] | None, Query()] = None,
) -> None:
    print("params: ", params)
    print("field: ", field)

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

Request:

curl --location 'localhost:8000/items?size=100&field=foo&field=bar'

Expected:

params:  size=100
field:  ['foo', 'bar']
INFO:     127.0.0.1:55020 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

Actual:

params:  size=100
field:  ['foo', 'bar']
INFO:     127.0.0.1:55020 - "GET /items?size=100&field=foo&field=bar HTTP/1.1" 200 OK

Versions

├── fastapi v0.128.7
│   ├── annotated-doc v0.0.4
│   ├── pydantic v2.12.5
│   │   ├── annotated-types v0.7.0
│   │   ├── pydantic-core v2.41.5
│   │   │   └── typing-extensions v4.15.0
│   │   ├── typing-extensions v4.15.0
│   │   ├── typing-inspection v0.4.2
│   │   │   └── typing-extensions v4.15.0
│   │   └── email-validator v2.3.0 (extra: email)
│   │       ├── dnspython v2.8.0
│   │       └── idna v3.11
│   ├── starlette v0.52.1
│   │   ├── anyio v4.12.1 (*)
│   │   └── typing-extensions v4.15.0
│   ├── typing-extensions v4.15.0
│   └── typing-inspection v0.4.2 (*)

extent analysis

Fix Plan

Problem Summary

The issue is that "complex" fields defined inside BaseModel are not being parsed correctly.

Root Cause Analysis

The root cause is likely due to the way Pydantic handles complex types in BaseModel.

Fix Plan

To fix this issue, we need to move the complex field out of the BaseModel and define it as a separate attribute.

Step-by-Step Solution

  1. Move the complex field out of BaseModel:

from typing import Annotated import uvicorn from fastapi import Depends, FastAPI, Query from pydantic import BaseModel

class MyParams(BaseModel): size: Annotated[int | None, Query()] = None

class MyComplexParams(BaseModel): field: Annotated[list[str] | None, Query()] = None

app = FastAPI()

@app.get("/items") async def get_items( params: Annotated[MyParams, Depends()], complex_params: Annotated[MyComplexParams, Depends()], ) -> None: print("params: ", params) print("complex_params: ", complex_params)


2. **Update the route to accept the new complex parameters**:
   ```python
@app.get("/items")
async def get_items(
    params: Annotated[MyParams, Depends()],
    field: Annotated[list[str] | None, Query()] = None,
) -> None:
    print("params: ", params)
    print("field: ", field)

Verification

To verify that the fix worked, you can run the application and send a request with the complex field:

curl --location 'localhost:8000/items?size=100&field=foo&field=bar'

The expected output should be:

params:  size=100
field:  ['foo', 'bar']
INFO:

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING