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
|
## Declare more metadata
|
||||||
|
|
||||||
You can add more information about the parameter.
|
You can add more information about the parameter.
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,13 @@ def is_scalar_field(field: Field) -> bool:
|
||||||
return (
|
return (
|
||||||
field.shape == Shape.SINGLETON
|
field.shape == Shape.SINGLETON
|
||||||
and not lenient_issubclass(field.type_, BaseModel)
|
and not lenient_issubclass(field.type_, BaseModel)
|
||||||
|
and not lenient_issubclass(field.type_, sequence_types + (dict,))
|
||||||
and not isinstance(field.schema, params.Body)
|
and not isinstance(field.schema, params.Body)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_scalar_sequence_field(field: Field) -> bool:
|
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
|
field.type_, BaseModel
|
||||||
):
|
):
|
||||||
if field.sub_fields is not None:
|
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):
|
if not is_scalar_field(sub_field):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
if lenient_issubclass(field.type_, sequence_types):
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -346,7 +349,7 @@ def request_params_to_args(
|
||||||
values = {}
|
values = {}
|
||||||
errors = []
|
errors = []
|
||||||
for field in required_params:
|
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)
|
received_params, (QueryParams, Headers)
|
||||||
):
|
):
|
||||||
value = received_params.getlist(field.alias) or field.default
|
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
|
import pytest
|
||||||
from fastapi import FastAPI, Query
|
from fastapi import FastAPI, Query
|
||||||
|
|
@ -27,3 +27,27 @@ def test_invalid_tuple():
|
||||||
@app.get("/items/")
|
@app.get("/items/")
|
||||||
def read_items(q: Tuple[Item, Item] = Query(None)):
|
def read_items(q: Tuple[Item, Item] = Query(None)):
|
||||||
pass # pragma: no cover
|
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)
|
response = client.get(url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"q": ["foo", "bar"]}
|
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