mirror of https://github.com/tiangolo/fastapi.git
🐛 Fix path and query parameters receiving dict as valid (#287)
* 🐛 Fix path and query parameters accepting dict * ✅ Add several tests to ensure invalid types are not accepted * 📝 Document (to include tested source) using query params with list * 🐛 Fix OpenAPI schema in query with list tutorial
This commit is contained in:
parent
2a7ef5504a
commit
c7db2ff858
|
|
@ -0,0 +1,9 @@
|
|||
from fastapi import FastAPI, Query
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(q: list = Query(None)):
|
||||
query_items = {"q": q}
|
||||
return query_items
|
||||
|
|
@ -183,6 +183,19 @@ the default of `q` will be: `["foo", "bar"]` and your response will be:
|
|||
}
|
||||
```
|
||||
|
||||
#### Using `list`
|
||||
|
||||
You can also use `list` directly instead of `List[str]`:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/query_params_str_validations/tutorial013.py!}
|
||||
```
|
||||
|
||||
!!! note
|
||||
Have in mind that in this case, FastAPI won't check the contents of the list.
|
||||
|
||||
For example, `List[int]` would check (and document) that the contents of the list are integers. But `list` alone wouldn't.
|
||||
|
||||
## Declare more metadata
|
||||
|
||||
You can add more information about the parameter.
|
||||
|
|
|
|||
|
|
@ -127,12 +127,13 @@ def is_scalar_field(field: Field) -> bool:
|
|||
return (
|
||||
field.shape == Shape.SINGLETON
|
||||
and not lenient_issubclass(field.type_, BaseModel)
|
||||
and not lenient_issubclass(field.type_, sequence_types + (dict,))
|
||||
and not isinstance(field.schema, params.Body)
|
||||
)
|
||||
|
||||
|
||||
def is_scalar_sequence_field(field: Field) -> bool:
|
||||
if field.shape in sequence_shapes and not lenient_issubclass(
|
||||
if (field.shape in sequence_shapes) and not lenient_issubclass(
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None:
|
||||
|
|
@ -140,6 +141,8 @@ def is_scalar_sequence_field(field: Field) -> bool:
|
|||
if not is_scalar_field(sub_field):
|
||||
return False
|
||||
return True
|
||||
if lenient_issubclass(field.type_, sequence_types):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -346,7 +349,7 @@ def request_params_to_args(
|
|||
values = {}
|
||||
errors = []
|
||||
for field in required_params:
|
||||
if field.shape in sequence_shapes and isinstance(
|
||||
if is_scalar_sequence_field(field) and isinstance(
|
||||
received_params, (QueryParams, Headers)
|
||||
):
|
||||
value = received_params.getlist(field.alias) or field.default
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
from typing import Dict, List, Tuple
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def test_invalid_sequence():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: List[Item]):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_tuple():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: Tuple[Item, Item]):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_dict():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: Dict[str, Item]):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_list():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: list):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_tuple():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: tuple):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_set():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: set):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_dict():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/items/{id}")
|
||||
def read_items(id: dict):
|
||||
pass # pragma: no cover
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List, Tuple
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, Query
|
||||
|
|
@ -27,3 +27,27 @@ def test_invalid_tuple():
|
|||
@app.get("/items/")
|
||||
def read_items(q: Tuple[Item, Item] = Query(None)):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_dict():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: Dict[str, Item] = Query(None)):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def test_invalid_simple_dict():
|
||||
with pytest.raises(AssertionError):
|
||||
app = FastAPI()
|
||||
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: dict = Query(None)):
|
||||
pass # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -86,3 +86,10 @@ def test_multi_query_values():
|
|||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": ["foo", "bar"]}
|
||||
|
||||
|
||||
def test_query_no_values():
|
||||
url = "/items/"
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": None}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
from starlette.testclient import TestClient
|
||||
|
||||
from query_params_str_validations.tutorial013 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Read Items",
|
||||
"operationId": "read_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "Q", "type": "array"},
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_multi_query_values():
|
||||
url = "/items/?q=foo&q=bar"
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": ["foo", "bar"]}
|
||||
|
||||
|
||||
def test_query_no_values():
|
||||
url = "/items/"
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": None}
|
||||
Loading…
Reference in New Issue