FastAPI List Query Parameters and File Uploads With Pydantic
To accept a list in a query string, declare the parameter as List[str] and wrap its default in Query(). Otherwise, FastAPI treats it as a request body and you get a 422. To accept multiple files, declare List[UploadFile] with File(...). You cannot use Pydantic models directly for multipart form data or repeated query keys without extra wiring.
from typing import List
from fastapi import FastAPI, Query
app = FastAPI()
# Correct: Query() tells FastAPI this is a repeated query param.
@app.get("/items")
def list_items(tags: List[str] = Query(default=[])):
return {"tags": tags}
Call this with /items?tags=red&tags=blue and you get ["red", "blue"]. If you omit Query(), FastAPI sees a bare List[str] annotation and assumes a JSON body, which fails validation on a GET request with a cryptic field required error.
Why the Bare List Annotation Breaks
FastAPI infers parameter location from the type. Scalars like str and int default to query parameters. Complex types like List, Dict, and Pydantic models default to the request body. The Query, Path, Header, and Cookie markers override this inference. This is the single most common source of fastapi list query parameter not working bug reports.
# Wrong: parsed as JSON body, 422 on GET requests.
@app.get("/broken")
def broken(tags: List[str] = []):
return tags
# Also wrong: Query without List only accepts the last value.
@app.get("/also-broken")
def also_broken(tags: str = Query(default="")):
return tags
Validating List Query Params With Constraints
You can add length and item validation directly on Query. For per-item validation, use Annotated with a constrained type so Pydantic validates each element.
from typing import Annotated, List
from fastapi import FastAPI, Query
from pydantic import Field
app = FastAPI()
@app.get("/search")
def search(
# Require 1 to 5 tags, each 2 to 20 chars.
tags: Annotated[List[str], Query(min_length=1, max_length=5)] = ["all"],
ids: Annotated[List[int], Query()] = [],
):
return {"tags": tags, "ids": ids}
Accepting Multiple File Uploads
Use List[UploadFile] with File(...). The client sends the same field name multiple times in a multipart/form-data body. Do not put UploadFile inside a Pydantic model, because OpenAPI multipart handling does not compose with Pydantic the way JSON bodies do.
from typing import List
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/upload")
async def upload(files: List[UploadFile] = File(...)):
results = []
for f in files:
# Read in chunks for large files rather than .read() all at once.
contents = await f.read()
results.append({"name": f.filename, "size": len(contents), "type": f.content_type})
return results
Mixing Files With Form Fields and Pydantic Data
A common need is uploading files plus structured metadata in one request. You cannot mix File with a JSON body, because multipart and JSON are different content types. The fix is to accept the metadata as a JSON string form field and parse it into a Pydantic model inside the handler.
import json
from typing import List
from fastapi import FastAPI, File, Form, UploadFile, HTTPException
from pydantic import BaseModel, ValidationError
class UploadMetadata(BaseModel):
title: str
tags: List[str] = []
app = FastAPI()
@app.post("/documents")
async def create_document(
metadata: str = Form(...),
files: List[UploadFile] = File(...),
):
try:
parsed = UploadMetadata.model_validate_json(metadata)
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors())
return {"title": parsed.title, "tags": parsed.tags, "count": len(files)}
Using a Pydantic Model for Query Parameters
Starting with FastAPI 0.115, you can group query parameters into a Pydantic model with Query() as the default. This reads cleaner than a long parameter list and gives you shared validators. Lists inside the model still work with repeated query keys.
from typing import Annotated, List
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
class ItemFilters(BaseModel):
# model_config forbids extra keys so typos 400 instead of silently passing.
model_config = {"extra": "forbid"}
tags: List[str] = Field(default_factory=list)
limit: int = Field(default=20, ge=1, le=100)
app = FastAPI()
@app.get("/items")
def list_items(filters: Annotated[ItemFilters, Query()]):
return filters
Gotchas Worth Remembering
Install python-multipart or file upload endpoints fail at import time with a clear error. Swagger UI sends multiple files correctly only when you declare List[UploadFile]. A single UploadFile with multiple selections silently keeps the last file. When a list query param is optional, use Query(default=None) with Optional[List[str]] rather than [], because a mutable default on the same function object can cause confusing behavior in middleware that introspects signatures. Finally, if you need comma-separated values like ?tags=a,b,c instead of repeated keys, parse the string yourself with a validator, because FastAPI does not split on commas by default.