Add set of tests for request parameters and alias (#14358)

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
This commit is contained in:
Motov Yurii 2025-12-11 17:15:36 +01:00 committed by GitHub
parent 475ce41268
commit 6c54bcefd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 10747 additions and 0 deletions

View File

View File

@ -0,0 +1,523 @@
from typing import List, Union
import pytest
from dirty_equals import IsDict, IsOneOf, IsPartialDict
from fastapi import Body, FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-list-str", operation_id="required_list_str")
async def read_required_list_str(p: Annotated[List[str], Body(embed=True)]):
return {"p": p}
class BodyModelRequiredListStr(BaseModel):
p: List[str]
@app.post("/model-required-list-str", operation_id="model_required_list_str")
def read_model_required_list_str(p: BodyModelRequiredListStr):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {
"items": {"type": "string"},
"title": "P",
"type": "array",
},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_missing(path: str, json: Union[dict, None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body", "p"], ["body"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
{
"detail": [
{
"loc": IsOneOf(["body", "p"], ["body"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.post("/required-list-alias", operation_id="required_list_alias")
async def read_required_list_alias(
p: Annotated[List[str], Body(embed=True, alias="p_alias")],
):
return {"p": p}
class BodyModelRequiredListAlias(BaseModel):
p: List[str] = Field(alias="p_alias")
@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
async def read_model_required_list_alias(p: BodyModelRequiredListAlias):
return {"p": p.p} # pragma: no cover
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 models",
strict=False,
),
),
"/model-required-list-alias",
],
)
def test_required_list_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {
"items": {"type": "string"},
"title": "P Alias",
"type": "array",
},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_missing(path: str, json: Union[dict, None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": ["hello", "world"]}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.post(
"/required-list-validation-alias", operation_id="required_list_validation_alias"
)
def read_required_list_validation_alias(
p: Annotated[List[str], Body(embed=True, validation_alias="p_val_alias")],
):
return {"p": p}
class BodyModelRequiredListValidationAlias(BaseModel):
p: List[str] = Field(validation_alias="p_val_alias")
@app.post(
"/model-required-list-validation-alias",
operation_id="model_required_list_validation_alias",
)
async def read_model_required_list_validation_alias(
p: BodyModelRequiredListValidationAlias,
):
return {"p": p.p} # pragma: no cover
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"title": "P Val Alias",
"type": "array",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": IsOneOf( # /required-validation-alias fails here
["body"], ["body", "p_val_alias"]
),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 422, (
response.text # /required-list-validation-alias fails here
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, (
response.text # /required-list-validation-alias fails here
)
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-list-alias-and-validation-alias",
operation_id="required_list_alias_and_validation_alias",
)
def read_required_list_alias_and_validation_alias(
p: Annotated[
List[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
],
):
return {"p": p}
class BodyModelRequiredListAliasAndValidationAlias(BaseModel):
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-required-list-alias-and-validation-alias",
operation_id="model_required_list_alias_and_validation_alias",
)
def read_model_required_list_alias_and_validation_alias(
p: BodyModelRequiredListAliasAndValidationAlias,
):
return {"p": p.p} # pragma: no cover
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"title": "P Val Alias",
"type": "array",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_missing(path: str, json):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": IsOneOf( # /required-list-alias-and-validation-alias fails here
["body"], ["body", "p_val_alias"]
),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [ # /required-list-alias-and-validation-alias fails here
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {"p": ["hello", "world"]}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": ["hello", "world"]})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, (
response.text # /required-list-alias-and-validation-alias fails here
)
assert response.json() == {"p": ["hello", "world"]}

View File

@ -0,0 +1,600 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import Body, FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-list-str", operation_id="optional_list_str")
async def read_optional_list_str(
p: Annotated[Optional[List[str]], Body(embed=True)] = None,
):
return {"p": p}
class BodyModelOptionalListStr(BaseModel):
p: Optional[List[str]] = None
@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
async def read_model_optional_list_str(p: BodyModelOptionalListStr):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p": {"items": {"type": "string"}, "type": "array", "title": "P"},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_list_str_missing():
client = TestClient(app)
response = client.post("/optional-list-str")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_list_str_missing():
client = TestClient(app)
response = client.post("/model-optional-list-str")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.post("/optional-list-alias", operation_id="optional_list_alias")
async def read_optional_list_alias(
p: Annotated[Optional[List[str]], Body(embed=True, alias="p_alias")] = None,
):
return {"p": p}
class BodyModelOptionalListAlias(BaseModel):
p: Optional[List[str]] = Field(None, alias="p_alias")
@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
async def read_model_optional_list_alias(p: BodyModelOptionalListAlias):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
strict=False,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
),
),
"/model-optional-list-alias",
],
)
def test_optional_list_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_list_alias_missing():
client = TestClient(app)
response = client.post("/optional-list-alias")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_list_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-list-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.post(
"/optional-list-validation-alias", operation_id="optional_list_validation_alias"
)
def read_optional_list_validation_alias(
p: Annotated[
Optional[List[str]], Body(embed=True, validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class BodyModelOptionalListValidationAlias(BaseModel):
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
@app.post(
"/model-optional-list-validation-alias",
operation_id="model_optional_list_validation_alias",
)
def read_model_optional_list_validation_alias(
p: BodyModelOptionalListValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_list_validation_alias_missing():
client = TestClient(app)
response = client.post("/optional-list-validation-alias")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_list_validation_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-list-validation-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None} # /optional-list-validation-alias fails here
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == { # /optional-list-validation-alias fails here
"p": ["hello", "world"]
}
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-list-alias-and-validation-alias",
operation_id="optional_list_alias_and_validation_alias",
)
def read_optional_list_alias_and_validation_alias(
p: Annotated[
Optional[List[str]],
Body(embed=True, alias="p_alias", validation_alias="p_val_alias"),
] = None,
):
return {"p": p}
class BodyModelOptionalListAliasAndValidationAlias(BaseModel):
p: Optional[List[str]] = Field(
None, alias="p_alias", validation_alias="p_val_alias"
)
@app.post(
"/model-optional-list-alias-and-validation-alias",
operation_id="model_optional_list_alias_and_validation_alias",
)
def read_model_optional_list_alias_and_validation_alias(
p: BodyModelOptionalListAliasAndValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_list_alias_and_validation_alias_missing():
client = TestClient(app)
response = client.post("/optional-list-alias-and-validation-alias")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_list_alias_and_validation_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-list-alias-and-validation-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-list-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {
"p": [ # /optional-list-alias-and-validation-alias fails here
"hello",
"world",
]
}

View File

@ -0,0 +1,569 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import Body, FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-str", operation_id="optional_str")
async def read_optional_str(p: Annotated[Optional[str], Body(embed=True)] = None):
return {"p": p}
class BodyModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.post("/model-optional-str", operation_id="model_optional_str")
async def read_model_optional_str(p: BodyModelOptionalStr):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p": {"type": "string", "title": "P"},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_str_missing():
client = TestClient(app)
response = client.post("/optional-str")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_str_missing():
client = TestClient(app)
response = client.post("/model-optional-str")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.post("/optional-alias", operation_id="optional_alias")
async def read_optional_alias(
p: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None,
):
return {"p": p}
class BodyModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.post("/model-optional-alias", operation_id="model_optional_alias")
async def read_model_optional_alias(p: BodyModelOptionalAlias):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
strict=False,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
),
),
"/model-optional-alias",
],
)
def test_optional_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_alias": {"type": "string", "title": "P Alias"},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_alias_missing():
client = TestClient(app)
response = client.post("/optional-alias")
assert response.status_code == 200
assert response.json() == {"p": None}
def test_model_optional_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_model_optional_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
def read_optional_validation_alias(
p: Annotated[
Optional[str], Body(embed=True, validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class BodyModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.post(
"/model-optional-validation-alias", operation_id="model_optional_validation_alias"
)
def read_model_optional_validation_alias(
p: BodyModelOptionalValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {"type": "string", "title": "P Val Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
def test_optional_validation_alias_missing():
client = TestClient(app)
response = client.post("/optional-validation-alias")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
def test_model_optional_validation_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-validation-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_model_optional_validation_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None} # /optional-validation-alias fails here
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-alias-and-validation-alias",
operation_id="optional_alias_and_validation_alias",
)
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class BodyModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-optional-alias-and-validation-alias",
operation_id="model_optional_alias_and_validation_alias",
)
def read_model_optional_alias_and_validation_alias(
p: BodyModelOptionalAliasAndValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {"type": "string", "title": "P Val Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
def test_optional_alias_and_validation_alias_missing():
client = TestClient(app)
response = client.post("/optional-alias-and-validation-alias")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
def test_model_optional_alias_and_validation_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-alias-and-validation-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_model_optional_alias_and_validation_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {
"p": "hello" # /optional-alias-and-validation-alias fails here
}

View File

@ -0,0 +1,514 @@
from typing import Any, Dict, Union
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import Body, FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-str", operation_id="required_str")
async def read_required_str(p: Annotated[str, Body(embed=True)]):
return {"p": p}
class BodyModelRequiredStr(BaseModel):
p: str
@app.post("/model-required-str", operation_id="model_required_str")
async def read_model_required_str(p: BodyModelRequiredStr):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {"title": "P", "type": "string"},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str, json: Union[Dict[str, Any], None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body"], ["body", "p"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": IsOneOf(["body"], ["body", "p"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.post("/required-alias", operation_id="required_alias")
async def read_required_alias(
p: Annotated[str, Body(embed=True, alias="p_alias")],
):
return {"p": p}
class BodyModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.post("/model-required-alias", operation_id="model_required_alias")
async def read_model_required_alias(p: BodyModelRequiredAlias):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
strict=False,
),
),
"/model-required-alias",
],
)
def test_required_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {"title": "P Alias", "type": "string"},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str, json: Union[Dict[str, Any], None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.post("/required-validation-alias", operation_id="required_validation_alias")
def read_required_validation_alias(
p: Annotated[str, Body(embed=True, validation_alias="p_val_alias")],
):
return {"p": p}
class BodyModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.post(
"/model-required-validation-alias", operation_id="model_required_validation_alias"
)
def read_model_required_validation_alias(
p: BodyModelRequiredValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {"title": "P Val Alias", "type": "string"},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(
path: str, json: Union[Dict[str, Any], None]
):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": IsOneOf( # /required-validation-alias fails here
["body", "p_val_alias"], ["body"]
),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 422, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": "hello"})
assert response.status_code == 200, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-alias-and-validation-alias",
operation_id="required_alias_and_validation_alias",
)
def read_required_alias_and_validation_alias(
p: Annotated[
str, Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
],
):
return {"p": p}
class BodyModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-required-alias-and-validation-alias",
operation_id="model_required_alias_and_validation_alias",
)
def read_model_required_alias_and_validation_alias(
p: BodyModelRequiredAliasAndValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {"title": "P Val Alias", "type": "string"},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(
path: str, json: Union[Dict[str, Any], None]
):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": IsOneOf( # /required-alias-and-validation-alias fails here
["body"], ["body", "p_val_alias"]
),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": "hello"})
assert response.status_code == 422, (
response.text # /required-alias-and-validation-alias fails here
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p_alias": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": "hello"})
assert response.status_code == 200, (
response.text # /required-alias-and-validation-alias fails here
)
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,7 @@
from typing import Any, Dict
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
body = openapi["paths"][path]["post"]["requestBody"]
body_schema = body["content"]["application/json"]["schema"]
return body_schema.get("$ref", "").split("/")[-1]

View File

@ -0,0 +1,3 @@
# Currently, there is no way to pass multiple cookies with the same name.
# The only way to pass multiple values for cookie params is to serialize them using
# a comma as a delimiter, but this is not currently supported by Starlette.

View File

@ -0,0 +1,3 @@
# Currently, there is no way to pass multiple cookies with the same name.
# The only way to pass multiple values for cookie params is to serialize them using
# a comma as a delimiter, but this is not currently supported by Starlette.

View File

@ -0,0 +1,383 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import Cookie, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-str")
async def read_optional_str(p: Annotated[Optional[str], Cookie()] = None):
return {"p": p}
class CookieModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.get("/model-optional-str")
async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
"name": "p",
"in": "cookie",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "cookie",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/optional-alias")
async def read_optional_alias(
p: Annotated[Optional[str], Cookie(alias="p_alias")] = None,
):
return {"p": p}
class CookieModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.get("/model-optional-alias")
async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cookie()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
"name": "p_alias",
"in": "cookie",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "cookie",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-alias",
pytest.param(
"/model-optional-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
client.cookies.set("p_alias", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"} # /model-optional-alias fails here
# =====================================================================================
# Validation alias
@app.get("/optional-validation-alias")
def read_optional_validation_alias(
p: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class CookieModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-validation-alias")
def read_model_optional_validation_alias(
p: Annotated[CookieModelOptionalValidationAlias, Cookie()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "cookie",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
client.cookies.set("p_val_alias", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
# =====================================================================================
# Alias and validation alias
@app.get("/optional-alias-and-validation-alias")
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class CookieModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-optional-alias-and-validation-alias")
def read_model_optional_alias_and_validation_alias(
p: Annotated[CookieModelOptionalAliasAndValidationAlias, Cookie()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "cookie",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
client.cookies.set("p_alias", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
client.cookies.set("p_val_alias", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {
"p": "hello" # /optional-alias-and-validation-alias fails here
}

View File

@ -0,0 +1,503 @@
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import Cookie, FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-str")
async def read_required_str(p: Annotated[str, Cookie()]):
return {"p": p}
class CookieModelRequiredStr(BaseModel):
p: str
@app.get("/model-required-str")
async def read_model_required_str(p: Annotated[CookieModelRequiredStr, Cookie()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "cookie",
}
]
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["cookie", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["cookie", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/required-alias")
async def read_required_alias(p: Annotated[str, Cookie(alias="p_alias")]):
return {"p": p}
class CookieModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.get("/model-required-alias")
async def read_model_required_alias(p: Annotated[CookieModelRequiredAlias, Cookie()]):
return {"p": p.p} # pragma: no cover
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "cookie",
}
]
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["cookie", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["cookie", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
pytest.param(
"/model-required-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 models",
strict=False,
),
),
],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["cookie", "p_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
{"p": "hello"}, # /model-required-alias PDv2 fails here
),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["cookie", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
pytest.param(
"/model-required-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
client.cookies.set("p_alias", "hello")
response = client.get(path)
assert response.status_code == 200, ( # /model-required-alias fails here
response.text
)
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/required-validation-alias")
def read_required_validation_alias(
p: Annotated[str, Cookie(validation_alias="p_val_alias")],
):
return {"p": p}
class CookieModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.get("/model-required-validation-alias")
def read_model_required_validation_alias(
p: Annotated[CookieModelRequiredValidationAlias, Cookie()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "cookie",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"cookie",
"p_val_alias", # /required-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 422, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["cookie", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
client.cookies.set("p_val_alias", "hello")
response = client.get(path)
assert response.status_code == 200, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/required-alias-and-validation-alias")
def read_required_alias_and_validation_alias(
p: Annotated[str, Cookie(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class CookieModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-alias-and-validation-alias")
def read_model_required_alias_and_validation_alias(
p: Annotated[CookieModelRequiredAliasAndValidationAlias, Cookie()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "cookie",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"cookie",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"cookie",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf( # /model-alias-and-validation-alias fails here
None,
{"p": "hello"},
),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
client.cookies.set("p_alias", "hello")
response = client.get(path)
assert (
response.status_code == 422 # /required-alias-and-validation-alias fails here
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["cookie", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf( # /model-alias-and-validation-alias fails here
None,
{"p_alias": "hello"},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
client.cookies.set("p_val_alias", "hello")
response = client.get(path)
assert response.status_code == 200, (
response.text # /required-alias-and-validation-alias fails here
)
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,597 @@
from typing import List
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, File, UploadFile
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/list-bytes", operation_id="list_bytes")
async def read_list_bytes(p: Annotated[List[bytes], File()]):
return {"file_size": [len(file) for file in p]}
@app.post("/list-uploadfile", operation_id="list_uploadfile")
async def read_list_uploadfile(p: Annotated[List[UploadFile], File()]):
return {"file_size": [file.size for file in p]}
@pytest.mark.parametrize(
"path",
[
"/list-bytes",
"/list-uploadfile",
],
)
def test_list_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P",
},
)
| IsDict(
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"title": "P",
},
)
)
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/list-bytes",
"/list-uploadfile",
],
)
def test_list_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/list-bytes",
"/list-uploadfile",
],
)
def test_list(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 200
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Alias
@app.post("/list-bytes-alias", operation_id="list_bytes_alias")
async def read_list_bytes_alias(p: Annotated[List[bytes], File(alias="p_alias")]):
return {"file_size": [len(file) for file in p]}
@app.post("/list-uploadfile-alias", operation_id="list_uploadfile_alias")
async def read_list_uploadfile_alias(
p: Annotated[List[UploadFile], File(alias="p_alias")],
):
return {"file_size": [file.size for file in p]}
@pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
strict=False,
)
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias",
"/list-uploadfile-alias",
],
)
def test_list_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Alias",
},
)
| IsDict(
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"title": "P Alias",
},
)
)
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias",
"/list-uploadfile-alias",
],
)
def test_list_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias",
"/list-uploadfile-alias",
],
)
def test_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias",
"/list-uploadfile-alias",
],
)
def test_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Validation alias
@app.post("/list-bytes-validation-alias", operation_id="list_bytes_validation_alias")
def read_list_bytes_validation_alias(
p: Annotated[List[bytes], File(validation_alias="p_val_alias")],
):
return {"file_size": [len(file) for file in p]}
@app.post(
"/list-uploadfile-validation-alias",
operation_id="list_uploadfile_validation_alias",
)
def read_list_uploadfile_validation_alias(
p: Annotated[List[UploadFile], File(validation_alias="p_val_alias")],
):
return {"file_size": [file.size for file in p]}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-validation-alias",
"/list-uploadfile-validation-alias",
],
)
def test_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Val Alias",
},
)
| IsDict(
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"title": "P Val Alias",
},
)
)
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/list-bytes-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/list-uploadfile-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [ # /list-*-validation-alias fail here
"body",
"p_val_alias",
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/list-bytes-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/list-uploadfile-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 422, ( # /list-*-validation-alias fail here
response.text
)
assert response.json() == { # pragma: no cover
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": None,
}
]
}
@pytest.mark.xfail(raises=AssertionError, strict=False)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-validation-alias",
"/list-uploadfile-validation-alias",
],
)
def test_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
)
assert response.status_code == 200, response.text # all 2 fail here
assert response.json() == {"file_size": [5, 5]} # pragma: no cover
# =====================================================================================
# Alias and validation alias
@app.post(
"/list-bytes-alias-and-validation-alias",
operation_id="list_bytes_alias_and_validation_alias",
)
def read_list_bytes_alias_and_validation_alias(
p: Annotated[List[bytes], File(alias="p_alias", validation_alias="p_val_alias")],
):
return {"file_size": [len(file) for file in p]}
@app.post(
"/list-uploadfile-alias-and-validation-alias",
operation_id="list_uploadfile_alias_and_validation_alias",
)
def read_list_uploadfile_alias_and_validation_alias(
p: Annotated[
List[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
],
):
return {"file_size": [file.size for file in p]}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias-and-validation-alias",
"/list-uploadfile-alias-and-validation-alias",
],
)
def test_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Val Alias",
},
)
| IsDict(
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"title": "P Val Alias",
},
)
)
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/list-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/list-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /list-*-alias-and-validation-alias fail here
],
"msg": "Field required",
"input": None,
}
]
}
@pytest.mark.xfail(raises=AssertionError, strict=False)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias-and-validation-alias",
"/list-uploadfile-alias-and-validation-alias",
],
)
def test_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", "hello"), ("p", "world")])
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /list-*-alias-and-validation-alias fail here
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/list-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/list-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
assert response.status_code == 422, (
response.text # /list-*-alias-and-validation-alias fails here
)
assert response.json() == { # pragma: no cover
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": None,
}
]
}
@pytest.mark.xfail(raises=AssertionError, strict=False)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias-and-validation-alias",
"/list-uploadfile-alias-and-validation-alias",
],
)
def test_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
)
assert response.status_code == 200, ( # all 2 fail here
response.text
)
assert response.json() == {"file_size": [5, 5]} # pragma: no cover

View File

@ -0,0 +1,443 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, File, UploadFile
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-bytes", operation_id="optional_bytes")
async def read_optional_bytes(p: Annotated[Optional[bytes], File()] = None):
return {"file_size": len(p) if p else None}
@app.post("/optional-uploadfile", operation_id="optional_uploadfile")
async def read_optional_uploadfile(p: Annotated[Optional[UploadFile], File()] = None):
return {"file_size": p.size if p else None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes",
"/optional-uploadfile",
],
)
def test_optional_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": (
IsDict(
{
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "P", "type": "string", "format": "binary"}
)
),
},
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes",
"/optional-uploadfile",
],
)
def test_optional_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes",
"/optional-uploadfile",
],
)
def test_optional(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 200
assert response.json() == {"file_size": 5}
# =====================================================================================
# Alias
@app.post("/optional-bytes-alias", operation_id="optional_bytes_alias")
async def read_optional_bytes_alias(
p: Annotated[Optional[bytes], File(alias="p_alias")] = None,
):
return {"file_size": len(p) if p else None}
@app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias")
async def read_optional_uploadfile_alias(
p: Annotated[Optional[UploadFile], File(alias="p_alias")] = None,
):
return {"file_size": p.size if p else None}
@pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
strict=False,
)
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias",
"/optional-uploadfile-alias",
],
)
def test_optional_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": (
IsDict(
{
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "P Alias", "type": "string", "format": "binary"}
)
),
},
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias",
"/optional-uploadfile-alias",
],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias",
"/optional-uploadfile-alias",
],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 200
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias",
"/optional-uploadfile-alias",
],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5}
# =====================================================================================
# Validation alias
@app.post(
"/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias"
)
def read_optional_bytes_validation_alias(
p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None,
):
return {"file_size": len(p) if p else None}
@app.post(
"/optional-uploadfile-validation-alias",
operation_id="optional_uploadfile_validation_alias",
)
def read_optional_uploadfile_validation_alias(
p: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None,
):
return {"file_size": p.size if p else None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-validation-alias",
"/optional-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Val Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "P Val Alias", "type": "string", "format": "binary"}
)
),
},
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-validation-alias",
"/optional-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-bytes-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/optional-uploadfile-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == { # /optional-*-validation-alias fail here
"file_size": None
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-bytes-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/optional-uploadfile-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_val_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5} # /optional-*-validation-alias fail here
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-bytes-alias-and-validation-alias",
operation_id="optional_bytes_alias_and_validation_alias",
)
def read_optional_bytes_alias_and_validation_alias(
p: Annotated[
Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"file_size": len(p) if p else None}
@app.post(
"/optional-uploadfile-alias-and-validation-alias",
operation_id="optional_uploadfile_alias_and_validation_alias",
)
def read_optional_uploadfile_alias_and_validation_alias(
p: Annotated[
Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"file_size": p.size if p else None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias-and-validation-alias",
"/optional-uploadfile-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Val Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "P Val Alias", "type": "string", "format": "binary"}
)
),
},
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias-and-validation-alias",
"/optional-uploadfile-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias-and-validation-alias",
"/optional-uploadfile-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/optional-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/optional-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_val_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {
"file_size": 5
} # /optional-*-alias-and-validation-alias fail here

View File

@ -0,0 +1,487 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, File, UploadFile
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-list-bytes")
async def read_optional_list_bytes(p: Annotated[Optional[List[bytes]], File()] = None):
return {"file_size": [len(file) for file in p] if p else None}
@app.post("/optional-list-uploadfile")
async def read_optional_list_uploadfile(
p: Annotated[Optional[List[UploadFile]], File()] = None,
):
return {"file_size": [file.size for file in p] if p else None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes",
"/optional-list-uploadfile",
],
)
def test_optional_list_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "P",
"type": "array",
"items": {"type": "string", "format": "binary"},
},
)
),
},
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes",
"/optional-list-uploadfile",
],
)
def test_optional_list_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-bytes",
marks=pytest.mark.xfail(
raises=(TypeError, AssertionError),
condition=PYDANTIC_V2,
reason="Fails only with PDv2 due to #14297",
strict=False,
),
),
"/optional-list-uploadfile",
],
)
def test_optional_list(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 200
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Alias
@app.post("/optional-list-bytes-alias")
async def read_optional_list_bytes_alias(
p: Annotated[Optional[List[bytes]], File(alias="p_alias")] = None,
):
return {"file_size": [len(file) for file in p] if p else None}
@app.post("/optional-list-uploadfile-alias")
async def read_optional_list_uploadfile_alias(
p: Annotated[Optional[List[UploadFile]], File(alias="p_alias")] = None,
):
return {"file_size": [file.size for file in p] if p else None}
@pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
strict=False,
)
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias",
"/optional-list-uploadfile-alias",
],
)
def test_optional_list_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "P Alias",
"type": "array",
"items": {"type": "string", "format": "binary"},
}
)
),
},
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias",
"/optional-list-uploadfile-alias",
],
)
def test_optional_list_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias",
"/optional-list-uploadfile-alias",
],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-bytes-alias",
marks=pytest.mark.xfail(
raises=(TypeError, AssertionError),
strict=False,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 model due to #14297",
),
),
"/optional-list-uploadfile-alias",
],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Validation alias
@app.post("/optional-list-bytes-validation-alias")
def read_optional_list_bytes_validation_alias(
p: Annotated[Optional[List[bytes]], File(validation_alias="p_val_alias")] = None,
):
return {"file_size": [len(file) for file in p] if p else None}
@app.post("/optional-list-uploadfile-validation-alias")
def read_optional_list_uploadfile_validation_alias(
p: Annotated[
Optional[List[UploadFile]], File(validation_alias="p_val_alias")
] = None,
):
return {"file_size": [file.size for file in p] if p else None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-validation-alias",
"/optional-list-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Val Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "P Val Alias",
"type": "array",
"items": {"type": "string", "format": "binary"},
}
)
),
},
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-validation-alias",
"/optional-list-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-bytes-validation-alias",
marks=pytest.mark.xfail(
raises=(TypeError, AssertionError),
strict=False,
reason="Fails due to #14297",
),
),
pytest.param(
"/optional-list-uploadfile-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 200, response.text
assert response.json() == { # /optional-list-uploadfile-validation-alias fails here
"file_size": None
}
@pytest.mark.xfail(raises=AssertionError, strict=False)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-validation-alias",
"/optional-list-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
)
assert response.status_code == 200, response.text
assert response.json() == {
"file_size": [5, 5] # /optional-list-*-validation-alias fail here
}
# =====================================================================================
# Alias and validation alias
@app.post("/optional-list-bytes-alias-and-validation-alias")
def read_optional_list_bytes_alias_and_validation_alias(
p: Annotated[
Optional[List[bytes]], File(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"file_size": [len(file) for file in p] if p else None}
@app.post("/optional-list-uploadfile-alias-and-validation-alias")
def read_optional_list_uploadfile_alias_and_validation_alias(
p: Annotated[
Optional[List[UploadFile]],
File(alias="p_alias", validation_alias="p_val_alias"),
] = None,
):
return {"file_size": [file.size for file in p] if p else None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Val Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "P Val Alias",
"type": "array",
"items": {"type": "string", "format": "binary"},
}
)
),
},
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(
raises=(TypeError, AssertionError),
strict=False,
reason="Fails due to #14297",
),
),
pytest.param(
"/optional-list-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
assert response.status_code == 200, response.text
assert ( # /optional-list-uploadfile-alias-and-validation-alias fails here
response.json() == {"file_size": None}
)
@pytest.mark.xfail(raises=AssertionError, strict=False)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
)
assert response.status_code == 200, response.text
assert response.json() == {
"file_size": [5, 5] # /optional-list-*-alias-and-validation-alias fail here
}

View File

@ -0,0 +1,536 @@
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, File, UploadFile
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-bytes", operation_id="required_bytes")
async def read_required_bytes(p: Annotated[bytes, File()]):
return {"file_size": len(p)}
@app.post("/required-uploadfile", operation_id="required_uploadfile")
async def read_required_uploadfile(p: Annotated[UploadFile, File()]):
return {"file_size": p.size}
@pytest.mark.parametrize(
"path",
[
"/required-bytes",
"/required-uploadfile",
],
)
def test_required_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {"title": "P", "type": "string", "format": "binary"},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/required-bytes",
"/required-uploadfile",
],
)
def test_required_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-bytes",
"/required-uploadfile",
],
)
def test_required(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 200
assert response.json() == {"file_size": 5}
# =====================================================================================
# Alias
@app.post("/required-bytes-alias", operation_id="required_bytes_alias")
async def read_required_bytes_alias(p: Annotated[bytes, File(alias="p_alias")]):
return {"file_size": len(p)}
@app.post("/required-uploadfile-alias", operation_id="required_uploadfile_alias")
async def read_required_uploadfile_alias(
p: Annotated[UploadFile, File(alias="p_alias")],
):
return {"file_size": p.size}
@pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
strict=False,
)
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias",
"/required-uploadfile-alias",
],
)
def test_required_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {"title": "P Alias", "type": "string", "format": "binary"},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias",
"/required-uploadfile-alias",
],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias",
"/required-uploadfile-alias",
],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias",
"/required-uploadfile-alias",
],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5}
# =====================================================================================
# Validation alias
@app.post(
"/required-bytes-validation-alias", operation_id="required_bytes_validation_alias"
)
def read_required_bytes_validation_alias(
p: Annotated[bytes, File(validation_alias="p_val_alias")],
):
return {"file_size": len(p)}
@app.post(
"/required-uploadfile-validation-alias",
operation_id="required_uploadfile_validation_alias",
)
def read_required_uploadfile_validation_alias(
p: Annotated[UploadFile, File(validation_alias="p_val_alias")],
):
return {"file_size": p.size}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-validation-alias",
"/required-uploadfile-validation-alias",
],
)
def test_required_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"title": "P Val Alias",
"type": "string",
"format": "binary",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-bytes-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/required-uploadfile-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [ # /required-*-validation-alias fail here
"body",
"p_val_alias",
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-bytes-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/required-uploadfile-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 422, ( # /required-*-validation-alias fail here
response.text
)
assert response.json() == { # pragma: no cover
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-bytes-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/required-uploadfile-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_val_alias", b"hello")])
assert response.status_code == 200, ( # all 2 fail here
response.text
)
assert response.json() == {"file_size": 5} # pragma: no cover
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-bytes-alias-and-validation-alias",
operation_id="required_bytes_alias_and_validation_alias",
)
def read_required_bytes_alias_and_validation_alias(
p: Annotated[bytes, File(alias="p_alias", validation_alias="p_val_alias")],
):
return {"file_size": len(p)}
@app.post(
"/required-uploadfile-alias-and-validation-alias",
operation_id="required_uploadfile_alias_and_validation_alias",
)
def read_required_uploadfile_alias_and_validation_alias(
p: Annotated[UploadFile, File(alias="p_alias", validation_alias="p_val_alias")],
):
return {"file_size": p.size}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias-and-validation-alias",
"/required-uploadfile-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"title": "P Val Alias",
"type": "string",
"format": "binary",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/required-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /required-*-alias-and-validation-alias fail here
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/required-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files={"p": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /required-*-alias-and-validation-alias fail here
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/required-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello")])
assert response.status_code == 422, (
response.text # /required-*-alias-and-validation-alias fails here
)
assert response.json() == { # pragma: no cover
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-bytes-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
pytest.param(
"/required-uploadfile-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_val_alias", b"hello")])
assert response.status_code == 200, ( # all 2 fail here
response.text
)
assert response.json() == {"file_size": 5} # pragma: no cover

View File

@ -0,0 +1,7 @@
from typing import Any, Dict
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
body = openapi["paths"][path]["post"]["requestBody"]
body_schema = body["content"]["multipart/form-data"]["schema"]
return body_schema.get("$ref", "").split("/")[-1]

View File

@ -0,0 +1,527 @@
from typing import List
import pytest
from dirty_equals import IsDict, IsOneOf, IsPartialDict
from fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-list-str", operation_id="required_list_str")
async def read_required_list_str(p: Annotated[List[str], Form()]):
return {"p": p}
class FormModelRequiredListStr(BaseModel):
p: List[str]
@app.post("/model-required-list-str", operation_id="model_required_list_str")
def read_model_required_list_str(p: Annotated[FormModelRequiredListStr, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {
"items": {"type": "string"},
"title": "P",
"type": "array",
},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
{
"detail": [
{
"loc": ["body", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.post("/required-list-alias", operation_id="required_list_alias")
async def read_required_list_alias(p: Annotated[List[str], Form(alias="p_alias")]):
return {"p": p}
class FormModelRequiredListAlias(BaseModel):
p: List[str] = Field(alias="p_alias")
@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
async def read_model_required_list_alias(
p: Annotated[FormModelRequiredListAlias, Form()],
):
return {"p": p.p} # pragma: no cover
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 models",
strict=False,
),
),
"/model-required-list-alias",
],
)
def test_required_list_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {
"items": {"type": "string"},
"title": "P Alias",
"type": "array",
},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
pytest.param(
"/model-required-list-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 models",
strict=False,
),
),
],
)
def test_required_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf( # /model-required-list-alias with PDv2 fails here
None, {"p": ["hello", "world"]}
),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.post(
"/required-list-validation-alias", operation_id="required_list_validation_alias"
)
def read_required_list_validation_alias(
p: Annotated[List[str], Form(validation_alias="p_val_alias")],
):
return {"p": p}
class FormModelRequiredListValidationAlias(BaseModel):
p: List[str] = Field(validation_alias="p_val_alias")
@app.post(
"/model-required-list-validation-alias",
operation_id="model_required_list_validation_alias",
)
async def read_model_required_list_validation_alias(
p: Annotated[FormModelRequiredListValidationAlias, Form()],
):
return {"p": p.p} # pragma: no cover
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"title": "P Val Alias",
"type": "array",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /required-list-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 422, (
response.text # /required-list-validation-alias fails here
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text # both fail here
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-list-alias-and-validation-alias",
operation_id="required_list_alias_and_validation_alias",
)
def read_required_list_alias_and_validation_alias(
p: Annotated[List[str], Form(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class FormModelRequiredListAliasAndValidationAlias(BaseModel):
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-required-list-alias-and-validation-alias",
operation_id="model_required_list_alias_and_validation_alias",
)
def read_model_required_list_alias_and_validation_alias(
p: Annotated[FormModelRequiredListAliasAndValidationAlias, Form()],
):
return {"p": p.p} # pragma: no cover
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"title": "P Val Alias",
"type": "array",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
# /required-list-alias-and-validation-alias fails here
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
# /required-list-alias-and-validation-alias fails here
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
# /model-required-list-alias-and-validation-alias fails here
{"p": ["hello", "world"]},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": ["hello", "world"]})
assert ( # /required-list-alias-and-validation-alias fails here
response.status_code == 422
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text # both fail here
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover

View File

@ -0,0 +1,454 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-list-str", operation_id="optional_list_str")
async def read_optional_list_str(
p: Annotated[Optional[List[str]], Form()] = None,
):
return {"p": p}
class FormModelOptionalListStr(BaseModel):
p: Optional[List[str]] = None
@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
async def read_model_optional_list_str(p: Annotated[FormModelOptionalListStr, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p": {"items": {"type": "string"}, "type": "array", "title": "P"},
},
"title": body_model_name,
"type": "object",
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.post("/optional-list-alias", operation_id="optional_list_alias")
async def read_optional_list_alias(
p: Annotated[Optional[List[str]], Form(alias="p_alias")] = None,
):
return {"p": p}
class FormModelOptionalListAlias(BaseModel):
p: Optional[List[str]] = Field(None, alias="p_alias")
@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
async def read_model_optional_list_alias(
p: Annotated[FormModelOptionalListAlias, Form()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
strict=False,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
),
),
"/model-optional-list-alias",
],
)
def test_optional_list_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.post(
"/optional-list-validation-alias", operation_id="optional_list_validation_alias"
)
def read_optional_list_validation_alias(
p: Annotated[Optional[List[str]], Form(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class FormModelOptionalListValidationAlias(BaseModel):
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
@app.post(
"/model-optional-list-validation-alias",
operation_id="model_optional_list_validation_alias",
)
def read_model_optional_list_validation_alias(
p: Annotated[FormModelOptionalListValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None} # /optional-list-validation-alias fails here
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, (
response.text # /model-optional-list-validation-alias fails here
)
assert response.json() == { # /optional-list-validation-alias fails here
"p": ["hello", "world"]
}
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-list-alias-and-validation-alias",
operation_id="optional_list_alias_and_validation_alias",
)
def read_optional_list_alias_and_validation_alias(
p: Annotated[
Optional[List[str]], Form(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class FormModelOptionalListAliasAndValidationAlias(BaseModel):
p: Optional[List[str]] = Field(
None, alias="p_alias", validation_alias="p_val_alias"
)
@app.post(
"/model-optional-list-alias-and-validation-alias",
operation_id="model_optional_list_alias_and_validation_alias",
)
def read_model_optional_list_alias_and_validation_alias(
p: Annotated[FormModelOptionalListAliasAndValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-list-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, (
response.text # /model-optional-list-alias-and-validation-alias fails here
)
assert response.json() == {
"p": [ # /optional-list-alias-and-validation-alias fails here
"hello",
"world",
]
}

View File

@ -0,0 +1,419 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-str", operation_id="optional_str")
async def read_optional_str(p: Annotated[Optional[str], Form()] = None):
return {"p": p}
class FormModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.post("/model-optional-str", operation_id="model_optional_str")
async def read_model_optional_str(p: Annotated[FormModelOptionalStr, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p": {"type": "string", "title": "P"},
},
"title": body_model_name,
"type": "object",
}
)
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.post("/optional-alias", operation_id="optional_alias")
async def read_optional_alias(
p: Annotated[Optional[str], Form(alias="p_alias")] = None,
):
return {"p": p}
class FormModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.post("/model-optional-alias", operation_id="model_optional_alias")
async def read_model_optional_alias(p: Annotated[FormModelOptionalAlias, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
strict=False,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
),
),
"/model-optional-alias",
],
)
def test_optional_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_alias": {"type": "string", "title": "P Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
def read_optional_validation_alias(
p: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class FormModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.post(
"/model-optional-validation-alias", operation_id="model_optional_validation_alias"
)
def read_model_optional_validation_alias(
p: Annotated[FormModelOptionalValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {"type": "string", "title": "P Val Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None} # /optional-validation-alias fails here
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-alias-and-validation-alias",
operation_id="optional_alias_and_validation_alias",
)
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Form(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class FormModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-optional-alias-and-validation-alias",
operation_id="model_optional_alias_and_validation_alias",
)
def read_model_optional_alias_and_validation_alias(
p: Annotated[FormModelOptionalAliasAndValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {"type": "string", "title": "P Val Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {
"p": "hello" # /optional-alias-and-validation-alias fails here
}

View File

@ -0,0 +1,502 @@
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-str", operation_id="required_str")
async def read_required_str(p: Annotated[str, Form()]):
return {"p": p}
class FormModelRequiredStr(BaseModel):
p: str
@app.post("/model-required-str", operation_id="model_required_str")
async def read_model_required_str(p: Annotated[FormModelRequiredStr, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {"title": "P", "type": "string"},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.post("/required-alias", operation_id="required_alias")
async def read_required_alias(p: Annotated[str, Form(alias="p_alias")]):
return {"p": p}
class FormModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.post("/model-required-alias", operation_id="model_required_alias")
async def read_model_required_alias(p: Annotated[FormModelRequiredAlias, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2",
strict=False,
),
),
"/model-required-alias",
],
)
def test_required_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {"title": "P Alias", "type": "string"},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.post("/required-validation-alias", operation_id="required_validation_alias")
def read_required_validation_alias(
p: Annotated[str, Form(validation_alias="p_val_alias")],
):
return {"p": p}
class FormModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.post(
"/model-required-validation-alias", operation_id="model_required_validation_alias"
)
def read_model_required_validation_alias(
p: Annotated[FormModelRequiredValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {"title": "P Val Alias", "type": "string"},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /required-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 422, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": "hello"})
assert response.status_code == 200, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-alias-and-validation-alias",
operation_id="required_alias_and_validation_alias",
)
def read_required_alias_and_validation_alias(
p: Annotated[str, Form(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class FormModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-required-alias-and-validation-alias",
operation_id="model_required_alias_and_validation_alias",
)
def read_model_required_alias_and_validation_alias(
p: Annotated[FormModelRequiredAliasAndValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {"title": "P Val Alias", "type": "string"},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": "hello"})
assert response.status_code == 422, (
response.text # /required-alias-and-validation-alias fails here
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p_alias": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": "hello"})
assert response.status_code == 200, (
response.text # /required-alias-and-validation-alias fails here
)
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,7 @@
from typing import Any, Dict
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
body = openapi["paths"][path]["post"]["requestBody"]
body_schema = body["content"]["application/x-www-form-urlencoded"]["schema"]
return body_schema.get("$ref", "").split("/")[-1]

View File

@ -0,0 +1,505 @@
from typing import List
import pytest
from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
from fastapi import FastAPI, Header
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-list-str")
async def read_required_list_str(p: Annotated[List[str], Header()]):
return {"p": p}
class HeaderModelRequiredListStr(BaseModel):
p: List[str]
@app.get("/model-required-list-str")
def read_model_required_list_str(p: Annotated[HeaderModelRequiredListStr, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P",
"type": "array",
"items": {"type": "string"},
},
"name": "p",
"in": "header",
}
]
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p"],
"msg": "Field required",
"input": AnyThing,
}
]
}
) | IsDict(
{
"detail": [
{
"loc": ["header", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.get("/required-list-alias")
async def read_required_list_alias(p: Annotated[List[str], Header(alias="p_alias")]):
return {"p": p}
class HeaderModelRequiredListAlias(BaseModel):
p: List[str] = Field(alias="p_alias")
@app.get("/model-required-list-alias")
async def read_model_required_list_alias(
p: Annotated[HeaderModelRequiredListAlias, Header()],
):
return {"p": p.p} # pragma: no cover
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_alias",
"in": "header",
}
]
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p_alias"],
"msg": "Field required",
"input": AnyThing,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
pytest.param(
"/model-required-list-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 models",
strict=False,
),
),
],
)
def test_required_list_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p_alias"],
"msg": "Field required",
"input": IsOneOf( # /model-required-list-alias with PDv2 fails here
None, IsPartialDict({"p": ["hello", "world"]})
),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
pytest.param(
"/model-required-list-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
assert response.status_code == 200, ( # /model-required-list-alias fails here
response.text
)
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.get("/required-list-validation-alias")
def read_required_list_validation_alias(
p: Annotated[List[str], Header(validation_alias="p_val_alias")],
):
return {"p": p}
class HeaderModelRequiredListValidationAlias(BaseModel):
p: List[str] = Field(validation_alias="p_val_alias")
@app.get("/model-required-list-validation-alias")
async def read_model_required_list_validation_alias(
p: Annotated[HeaderModelRequiredListValidationAlias, Header()],
):
return {"p": p.p} # pragma: no cover
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Val Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias", # /required-list-validation-alias fails here
],
"msg": "Field required",
"input": AnyThing,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 422 # /required-list-validation-alias fails here
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["header", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
)
assert response.status_code == 200, response.text # both fail here
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
# =====================================================================================
# Alias and validation alias
@app.get("/required-list-alias-and-validation-alias")
def read_required_list_alias_and_validation_alias(
p: Annotated[List[str], Header(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class HeaderModelRequiredListAliasAndValidationAlias(BaseModel):
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-list-alias-and-validation-alias")
def read_model_required_list_alias_and_validation_alias(
p: Annotated[HeaderModelRequiredListAliasAndValidationAlias, Header()],
):
return {"p": p.p} # pragma: no cover
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Val Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
# /required-list-alias-and-validation-alias fails here
"p_val_alias",
],
"msg": "Field required",
"input": AnyThing,
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
# /required-list-alias-and-validation-alias fails here
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
# /model-required-list-alias-and-validation-alias fails here
IsPartialDict({"p": ["hello", "world"]}),
),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
assert ( # /required-list-alias-and-validation-alias fails here
response.status_code == 422
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["header", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
# /model-required-list-alias-and-validation-alias fails here
IsPartialDict({"p_alias": ["hello", "world"]}),
),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
)
assert response.status_code == 200, response.text # both fail here
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover

View File

@ -0,0 +1,407 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-list-str")
async def read_optional_list_str(
p: Annotated[Optional[List[str]], Header()] = None,
):
return {"p": p}
class HeaderModelOptionalListStr(BaseModel):
p: Optional[List[str]] = None
@app.get("/model-optional-list-str")
async def read_model_optional_list_str(
p: Annotated[HeaderModelOptionalListStr, Header()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P",
},
"name": "p",
"in": "header",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
"name": "p",
"in": "header",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.get("/optional-list-alias")
async def read_optional_list_alias(
p: Annotated[Optional[List[str]], Header(alias="p_alias")] = None,
):
return {"p": p}
class HeaderModelOptionalListAlias(BaseModel):
p: Optional[List[str]] = Field(None, alias="p_alias")
@app.get("/model-optional-list-alias")
async def read_model_optional_list_alias(
p: Annotated[HeaderModelOptionalListAlias, Header()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Alias",
},
"name": "p_alias",
"in": "header",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {
"items": {"type": "string"},
"type": "array",
"title": "P Alias",
},
"name": "p_alias",
"in": "header",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias",
pytest.param(
"/model-optional-list-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
assert response.status_code == 200
assert response.json() == {
"p": ["hello", "world"] # /model-optional-list-alias fails here
}
# =====================================================================================
# Validation alias
@app.get("/optional-list-validation-alias")
def read_optional_list_validation_alias(
p: Annotated[Optional[List[str]], Header(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class HeaderModelOptionalListValidationAlias(BaseModel):
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-list-validation-alias")
def read_model_optional_list_validation_alias(
p: Annotated[HeaderModelOptionalListValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": None} # /optional-list-validation-alias fails here
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
)
assert response.status_code == 200, (
response.text # /model-optional-list-validation-alias fails here
)
assert response.json() == { # /optional-list-validation-alias fails here
"p": ["hello", "world"]
}
# =====================================================================================
# Alias and validation alias
@app.get("/optional-list-alias-and-validation-alias")
def read_optional_list_alias_and_validation_alias(
p: Annotated[
Optional[List[str]], Header(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class HeaderModelOptionalListAliasAndValidationAlias(BaseModel):
p: Optional[List[str]] = Field(
None, alias="p_alias", validation_alias="p_val_alias"
)
@app.get("/model-optional-list-alias-and-validation-alias")
def read_model_optional_list_alias_and_validation_alias(
p: Annotated[HeaderModelOptionalListAliasAndValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-list-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
)
assert response.status_code == 200, (
response.text # /model-optional-list-alias-and-validation-alias fails here
)
assert response.json() == {
"p": [ # /optional-list-alias-and-validation-alias fails here
"hello",
"world",
]
}

View File

@ -0,0 +1,375 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-str")
async def read_optional_str(p: Annotated[Optional[str], Header()] = None):
return {"p": p}
class HeaderModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.get("/model-optional-str")
async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
"name": "p",
"in": "header",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "header",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/optional-alias")
async def read_optional_alias(
p: Annotated[Optional[str], Header(alias="p_alias")] = None,
):
return {"p": p}
class HeaderModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.get("/model-optional-alias")
async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
"name": "p_alias",
"in": "header",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "header",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-alias",
pytest.param(
"/model-optional-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"} # /model-optional-alias fails here
# =====================================================================================
# Validation alias
@app.get("/optional-validation-alias")
def read_optional_validation_alias(
p: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class HeaderModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-validation-alias")
def read_model_optional_validation_alias(
p: Annotated[HeaderModelOptionalValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None} # /optional-validation-alias fails here
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
# =====================================================================================
# Alias and validation alias
@app.get("/optional-alias-and-validation-alias")
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Header(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class HeaderModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-optional-alias-and-validation-alias")
def read_model_optional_alias_and_validation_alias(
p: Annotated[HeaderModelOptionalAliasAndValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {
"p": "hello" # /optional-alias-and-validation-alias fails here
}

View File

@ -0,0 +1,492 @@
import pytest
from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
from fastapi import FastAPI, Header
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-str")
async def read_required_str(p: Annotated[str, Header()]):
return {"p": p}
class HeaderModelRequiredStr(BaseModel):
p: str
@app.get("/model-required-str")
async def read_model_required_str(p: Annotated[HeaderModelRequiredStr, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "header",
}
]
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p"],
"msg": "Field required",
"input": AnyThing,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/required-alias")
async def read_required_alias(p: Annotated[str, Header(alias="p_alias")]):
return {"p": p}
class HeaderModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.get("/model-required-alias")
async def read_model_required_alias(p: Annotated[HeaderModelRequiredAlias, Header()]):
return {"p": p.p} # pragma: no cover
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "header",
}
]
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p_alias"],
"msg": "Field required",
"input": AnyThing,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
pytest.param(
"/model-required-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 models",
strict=False,
),
),
],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": "hello"})),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
pytest.param(
"/model-required-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_alias": "hello"})
assert response.status_code == 200, ( # /model-required-alias fails here
response.text
)
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/required-validation-alias")
def read_required_validation_alias(
p: Annotated[str, Header(validation_alias="p_val_alias")],
):
return {"p": p}
class HeaderModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.get("/model-required-validation-alias")
def read_model_required_validation_alias(
p: Annotated[HeaderModelRequiredValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias", # /required-validation-alias fails here
],
"msg": "Field required",
"input": AnyThing,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 422, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["header", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": "hello"})),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_val_alias": "hello"})
assert response.status_code == 200, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/required-alias-and-validation-alias")
def read_required_alias_and_validation_alias(
p: Annotated[str, Header(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class HeaderModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-alias-and-validation-alias")
def read_model_required_alias_and_validation_alias(
p: Annotated[HeaderModelRequiredAliasAndValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": AnyThing,
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf( # /model-alias-and-validation-alias fails here
None,
IsPartialDict({"p": "hello"}),
),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_alias": "hello"})
assert (
response.status_code == 422 # /required-alias-and-validation-alias fails here
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["header", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf( # /model-alias-and-validation-alias fails here
None,
IsPartialDict({"p_alias": "hello"}),
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_val_alias": "hello"})
assert response.status_code == 200, (
response.text # /required-alias-and-validation-alias fails here
)
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1 @@
# FastAPI doesn't currently support non-scalar Path parameters

View File

@ -0,0 +1 @@
# Optional Path parameters are not supported

View File

@ -0,0 +1 @@
# Optional Path parameters are not supported

View File

@ -0,0 +1,102 @@
import pytest
from fastapi import FastAPI, Path
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
@app.get("/required-str/{p}")
async def read_required_str(p: Annotated[str, Path()]):
return {"p": p}
@app.get("/required-alias/{p_alias}")
async def read_required_alias(p: Annotated[str, Path(alias="p_alias")]):
return {"p": p}
@app.get("/required-validation-alias/{p_val_alias}")
def read_required_validation_alias(
p: Annotated[str, Path(validation_alias="p_val_alias")],
):
return {"p": p} # pragma: no cover
@app.get("/required-alias-and-validation-alias/{p_val_alias}")
def read_required_alias_and_validation_alias(
p: Annotated[str, Path(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p} # pragma: no cover
@pytest.mark.parametrize(
("path", "expected_name", "expected_title"),
[
pytest.param("/required-str/{p}", "p", "P", id="required-str"),
pytest.param(
"/required-alias/{p_alias}", "p_alias", "P Alias", id="required-alias"
),
pytest.param(
"/required-validation-alias/{p_val_alias}",
"p_val_alias",
"P Val Alias",
id="required-validation-alias",
marks=(
needs_pydanticv2,
pytest.mark.xfail(raises=AssertionError, strict=False),
),
),
pytest.param(
"/required-alias-and-validation-alias/{p_val_alias}",
"p_val_alias",
"P Val Alias",
id="required-alias-and-validation-alias",
marks=(
needs_pydanticv2,
pytest.mark.xfail(raises=AssertionError, strict=False),
),
),
],
)
def test_schema(path: str, expected_name: str, expected_title: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": expected_title, "type": "string"},
"name": expected_name,
"in": "path",
}
]
@pytest.mark.parametrize(
"path",
[
pytest.param("/required-str", id="required-str"),
pytest.param("/required-alias", id="required-alias"),
pytest.param(
"/required-validation-alias",
id="required-validation-alias",
marks=(
needs_pydanticv2,
pytest.mark.xfail(raises=AssertionError, strict=False),
),
),
pytest.param(
"/required-alias-and-validation-alias",
id="required-alias-and-validation-alias",
marks=(
needs_pydanticv2,
pytest.mark.xfail(raises=AssertionError, strict=False),
),
),
],
)
def test_success(path: str):
client = TestClient(app)
response = client.get(f"{path}/hello")
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,506 @@
from typing import List
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-list-str")
async def read_required_list_str(p: Annotated[List[str], Query()]):
return {"p": p}
class QueryModelRequiredListStr(BaseModel):
p: List[str]
@app.get("/model-required-list-str")
def read_model_required_list_str(p: Annotated[QueryModelRequiredListStr, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P",
"type": "array",
"items": {"type": "string"},
},
"name": "p",
"in": "query",
}
]
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
{
"detail": [
{
"loc": ["query", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.get("/required-list-alias")
async def read_required_list_alias(p: Annotated[List[str], Query(alias="p_alias")]):
return {"p": p}
class QueryModelRequiredListAlias(BaseModel):
p: List[str] = Field(alias="p_alias")
@app.get("/model-required-list-alias")
async def read_model_required_list_alias(
p: Annotated[QueryModelRequiredListAlias, Query()],
):
return {"p": p.p} # pragma: no cover
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_alias",
"in": "query",
}
]
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
pytest.param(
"/model-required-list-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 models",
strict=False,
),
),
],
)
def test_required_list_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p_alias"],
"msg": "Field required",
"input": IsOneOf( # /model-required-list-alias with PDv2 fails here
None, {"p": ["hello", "world"]}
),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
pytest.param(
"/model-required-list-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello&p_alias=world")
assert response.status_code == 200, ( # /model-required-list-alias fails here
response.text
)
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.get("/required-list-validation-alias")
def read_required_list_validation_alias(
p: Annotated[List[str], Query(validation_alias="p_val_alias")],
):
return {"p": p}
class QueryModelRequiredListValidationAlias(BaseModel):
p: List[str] = Field(validation_alias="p_val_alias")
@app.get("/model-required-list-validation-alias")
async def read_model_required_list_validation_alias(
p: Annotated[QueryModelRequiredListValidationAlias, Query()],
):
return {"p": p.p} # pragma: no cover
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Val Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias", # /required-list-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 422 # /required-list-validation-alias fails here
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["query", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": ["hello", "world"]}),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
assert response.status_code == 200, response.text # both fail here
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
# =====================================================================================
# Alias and validation alias
@app.get("/required-list-alias-and-validation-alias")
def read_required_list_alias_and_validation_alias(
p: Annotated[List[str], Query(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class QueryModelRequiredListAliasAndValidationAlias(BaseModel):
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-list-alias-and-validation-alias")
def read_model_required_list_alias_and_validation_alias(
p: Annotated[QueryModelRequiredListAliasAndValidationAlias, Query()],
):
return {"p": p.p} # pragma: no cover
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Val Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
# /required-list-alias-and-validation-alias fails here
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
# /required-list-alias-and-validation-alias fails here
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
# /model-required-list-alias-and-validation-alias fails here
{
"p": [
"hello",
"world",
]
},
),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello&p_alias=world")
assert ( # /required-list-alias-and-validation-alias fails here
response.status_code == 422
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["query", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
# /model-required-list-alias-and-validation-alias fails here
{"p_alias": ["hello", "world"]},
),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
assert response.status_code == 200, response.text # both fail here
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover

View File

@ -0,0 +1,403 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-list-str")
async def read_optional_list_str(
p: Annotated[Optional[List[str]], Query()] = None,
):
return {"p": p}
class QueryModelOptionalListStr(BaseModel):
p: Optional[List[str]] = None
@app.get("/model-optional-list-str")
async def read_model_optional_list_str(
p: Annotated[QueryModelOptionalListStr, Query()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P",
},
"name": "p",
"in": "query",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
"name": "p",
"in": "query",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.get("/optional-list-alias")
async def read_optional_list_alias(
p: Annotated[Optional[List[str]], Query(alias="p_alias")] = None,
):
return {"p": p}
class QueryModelOptionalListAlias(BaseModel):
p: Optional[List[str]] = Field(None, alias="p_alias")
@app.get("/model-optional-list-alias")
async def read_model_optional_list_alias(
p: Annotated[QueryModelOptionalListAlias, Query()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Alias",
},
"name": "p_alias",
"in": "query",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {
"items": {"type": "string"},
"type": "array",
"title": "P Alias",
},
"name": "p_alias",
"in": "query",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias",
pytest.param(
"/model-optional-list-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello&p_alias=world")
assert response.status_code == 200
assert response.json() == {
"p": ["hello", "world"] # /model-optional-list-alias fails here
}
# =====================================================================================
# Validation alias
@app.get("/optional-list-validation-alias")
def read_optional_list_validation_alias(
p: Annotated[Optional[List[str]], Query(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class QueryModelOptionalListValidationAlias(BaseModel):
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-list-validation-alias")
def read_model_optional_list_validation_alias(
p: Annotated[QueryModelOptionalListValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": None} # /optional-list-validation-alias fails here
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
assert response.status_code == 200, (
response.text # /model-optional-list-validation-alias fails here
)
assert response.json() == { # /optional-list-validation-alias fails here
"p": ["hello", "world"]
}
# =====================================================================================
# Alias and validation alias
@app.get("/optional-list-alias-and-validation-alias")
def read_optional_list_alias_and_validation_alias(
p: Annotated[
Optional[List[str]], Query(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class QueryModelOptionalListAliasAndValidationAlias(BaseModel):
p: Optional[List[str]] = Field(
None, alias="p_alias", validation_alias="p_val_alias"
)
@app.get("/model-optional-list-alias-and-validation-alias")
def read_model_optional_list_alias_and_validation_alias(
p: Annotated[QueryModelOptionalListAliasAndValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-list-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello&p_alias=world")
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-list-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
assert response.status_code == 200, (
response.text # /model-optional-list-alias-and-validation-alias fails here
)
assert response.json() == {
"p": [ # /optional-list-alias-and-validation-alias fails here
"hello",
"world",
]
}

View File

@ -0,0 +1,375 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-str")
async def read_optional_str(p: Optional[str] = None):
return {"p": p}
class QueryModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.get("/model-optional-str")
async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
"name": "p",
"in": "query",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "query",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/optional-alias")
async def read_optional_alias(
p: Annotated[Optional[str], Query(alias="p_alias")] = None,
):
return {"p": p}
class QueryModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.get("/model-optional-alias")
async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
"name": "p_alias",
"in": "query",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "query",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-alias",
pytest.param(
"/model-optional-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"} # /model-optional-alias fails here
# =====================================================================================
# Validation alias
@app.get("/optional-validation-alias")
def read_optional_validation_alias(
p: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class QueryModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-validation-alias")
def read_model_optional_validation_alias(
p: Annotated[QueryModelOptionalValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
# =====================================================================================
# Alias and validation alias
@app.get("/optional-alias-and-validation-alias")
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Query(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class QueryModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-optional-alias-and-validation-alias")
def read_model_optional_alias_and_validation_alias(
p: Annotated[QueryModelOptionalAliasAndValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello")
assert response.status_code == 200
assert response.json() == {
"p": None # /optional-alias-and-validation-alias fails here
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/optional-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello")
assert response.status_code == 200
assert response.json() == {
"p": "hello" # /optional-alias-and-validation-alias fails here
}

View File

@ -0,0 +1,495 @@
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-str")
async def read_required_str(p: str):
return {"p": p}
class QueryModelRequiredStr(BaseModel):
p: str
@app.get("/model-required-str")
async def read_model_required_str(p: Annotated[QueryModelRequiredStr, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "query",
}
]
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/required-alias")
async def read_required_alias(p: Annotated[str, Query(alias="p_alias")]):
return {"p": p}
class QueryModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.get("/model-required-alias")
async def read_model_required_alias(p: Annotated[QueryModelRequiredAlias, Query()]):
return {"p": p.p} # pragma: no cover
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "query",
}
]
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
pytest.param(
"/model-required-alias",
marks=pytest.mark.xfail(
raises=AssertionError,
condition=PYDANTIC_V2,
reason="Fails only with PDv2 models",
strict=False,
),
),
],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
{"p": "hello"}, # /model-required-alias PDv2 fails here
),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
pytest.param(
"/model-required-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello")
assert response.status_code == 200, ( # /model-required-alias fails here
response.text
)
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/required-validation-alias")
def read_required_validation_alias(
p: Annotated[str, Query(validation_alias="p_val_alias")],
):
return {"p": p}
class QueryModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.get("/model-required-validation-alias")
def read_model_required_validation_alias(
p: Annotated[QueryModelRequiredValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias", # /required-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 422, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["query", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello")
assert response.status_code == 200, ( # /required-validation-alias fails here
response.text
)
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/required-alias-and-validation-alias")
def read_required_alias_and_validation_alias(
p: Annotated[str, Query(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class QueryModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-alias-and-validation-alias")
def read_model_required_alias_and_validation_alias(
p: Annotated[QueryModelRequiredAliasAndValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias", # /required-alias-and-validation-alias fails here
],
"msg": "Field required",
"input": IsOneOf( # /model-alias-and-validation-alias fails here
None,
{"p": "hello"},
),
}
]
}
@needs_pydanticv2
@pytest.mark.xfail(raises=AssertionError, strict=False)
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello")
assert (
response.status_code == 422 # /required-alias-and-validation-alias fails here
)
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["query", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf( # /model-alias-and-validation-alias fails here
None,
{"p_alias": "hello"},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/required-alias-and-validation-alias",
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
),
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello")
assert response.status_code == 200, (
response.text # /required-alias-and-validation-alias fails here
)
assert response.json() == {"p": "hello"}