From de7d6dfcb1f33eae4e8b7bf6d7d05b4b91fe577e Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Thu, 13 Nov 2025 15:20:13 +0100 Subject: [PATCH] Fix Pydantic v1 optional schema tests and file model tests --- .../test_file_field_aliases_pd_v1.py | 55 +++++++--------- .../test_file_model_aliases_pd_v1.py | 63 +++++-------------- .../test_form_field_aliases_pd_v1.py | 43 ++++++------- .../test_form_model_aliases_pd_v1.py | 48 ++++++-------- .../test_uploadfile_model_aliases_pd_v1.py | 20 +++--- 5 files changed, 88 insertions(+), 141 deletions(-) diff --git a/tests/test_aliases/test_file_field_aliases_pd_v1.py b/tests/test_aliases/test_file_field_aliases_pd_v1.py index f9de67317..b7cf89852 100644 --- a/tests/test_aliases/test_file_field_aliases_pd_v1.py +++ b/tests/test_aliases/test_file_field_aliases_pd_v1.py @@ -1,7 +1,7 @@ from typing import List, Optional -import pytest from fastapi import FastAPI, File +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from ..utils import needs_pydanticv1 @@ -13,7 +13,6 @@ app = FastAPI() # ===================================================================================== # File(alias=...) # Current situation: Works -# Schema generation for optional field and optional list fails due to issue likely not related to aliases # ------------------------------ # required field @@ -51,14 +50,15 @@ def test_required_field_alias_schema(): # ------------------------------ # optional field +if not PYDANTIC_V2: -@app.post("/optional-field-alias", operation_id="optional_field_alias") -async def optional_field_alias( - file: Optional[bytes] = File(None, alias="file_alias"), -): - if file is None: - return {"file_size": None} - return {"file_size": len(file)} + @app.post("/optional-field-alias", operation_id="optional_field_alias") + async def optional_field_alias( + file: Optional[bytes] = File(None, alias="file_alias", nullable=True), + ): + if file is None: + return {"file_size": None} + return {"file_size": len(file)} def test_optional_field_alias_by_name(): @@ -75,20 +75,17 @@ def test_optional_field_alias_by_alias(): assert resp.json() == {"file_size": 7} -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_optional_field_alias_schema(): openapi = app.openapi() body_schema = openapi["components"]["schemas"]["Body_optional_field_alias"] assert body_schema["properties"] == { "file_alias": { - "anyOf": [{"type": "string", "format": "binary"}, {"type": "null"}], + "type": "string", + "nullable": True, + "format": "binary", "title": "File Alias", } } - # Fails with: - # AssertionError: assert - # {'file_alias': {'type': 'string', 'format': 'binary', 'title': 'File Alias'}} == - # {'file_alias': {'anyOf': [{'type': 'string', 'format': 'binary'}, {'type': 'null'}], 'title': 'File Alias'}} # ------------------------------ @@ -137,13 +134,15 @@ def test_list_field_alias_schema(): # optional list field -@app.post("/optional-list-field-alias", operation_id="optional_list_field_alias") -async def optional_list_field_alias( - files: Optional[List[bytes]] = File(None, alias="files_alias"), -): - if files is None: - return {"file_sizes": None} - return {"file_sizes": [len(file) for file in files]} +if not PYDANTIC_V2: + + @app.post("/optional-list-field-alias", operation_id="optional_list_field_alias") + async def optional_list_field_alias( + files: Optional[List[bytes]] = File(None, alias="files_alias", nullable=True), + ): + if files is None: + return {"file_sizes": None} + return {"file_sizes": [len(file) for file in files]} def test_optional_list_field_alias_by_name(): @@ -165,20 +164,14 @@ def test_optional_list_field_alias_by_alias(): assert resp.json() == {"file_sizes": [8, 8]} -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_optional_list_field_alias_schema(): openapi = app.openapi() body_schema = openapi["components"]["schemas"]["Body_optional_list_field_alias"] assert body_schema["properties"] == { "files_alias": { - "anyOf": [ - {"items": {"type": "string", "format": "binary"}, "type": "array"}, - {"type": "null"}, - ], + "items": {"format": "binary", "type": "string"}, + "nullable": True, "title": "Files Alias", + "type": "array", }, } - # Fails with: - # AssertionError: assert - # {'files_alias': {'items': {'type': 'string', 'format': 'binary'}, 'type': 'array', 'title': 'Files Alias'}} == - # {'files_alias': {'anyOf': [{'items': {'type': 'string', 'format': 'binary'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Files Alias'}} diff --git a/tests/test_aliases/test_file_model_aliases_pd_v1.py b/tests/test_aliases/test_file_model_aliases_pd_v1.py index 8a5f4b4a1..b0e26a38b 100644 --- a/tests/test_aliases/test_file_model_aliases_pd_v1.py +++ b/tests/test_aliases/test_file_model_aliases_pd_v1.py @@ -1,10 +1,9 @@ from typing import List, Optional -import pytest -from fastapi import FastAPI, File +from fastapi import FastAPI, File, Form from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel, Field +from pydantic import BaseModel from ..utils import needs_pydanticv1 @@ -14,20 +13,18 @@ app = FastAPI() # ===================================================================================== # Field(alias=...) -# Current situation: doesn't work due to validation error +# Current situation: works # ------------------------------ # required field class RequiredFieldAliasModel(BaseModel): - file: bytes = Field(alias="file_alias") + file: bytes = File(alias="file_alias") @app.post("/required-field-alias-model") -async def required_field_alias_model( # pragma: no cover (remove `no cover` when bug fixed) - data: RequiredFieldAliasModel = File(...), -): +async def required_field_alias_model(data: RequiredFieldAliasModel = Form()): return {"file_size": len(data.file)} @@ -40,17 +37,11 @@ def test_required_field_alias_model_by_name(): assert "file_alias" in detail[0]["loc"] -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_required_field_alias_model_by_alias(): client = TestClient(app) resp = client.post("/required-field-alias-model", files={"file_alias": b"content"}) assert resp.status_code == 200, resp.text - # Fails with: - # AssertionError: assert 422 == 200 - # {"detail":[{"loc":["body","file_alias"],"msg":"byte type expected","type":"type_error.bytes"}]} - - # Uncomment when the assertion above passes: - # assert resp.json() == {"file_size": 7} + assert resp.json() == {"file_size": 7} def test_required_field_alias_model_schema(): @@ -67,12 +58,10 @@ def test_required_field_alias_model_schema(): if not PYDANTIC_V2: class OptionalFieldAliasModel(BaseModel): - file: Optional[bytes] = Field(None, alias="file_alias", nullable=True) + file: Optional[bytes] = File(None, alias="file_alias", nullable=True) @app.post("/optional-field-alias-model") - async def optional_field_alias_model( # pragma: no cover (remove `no cover` when bug fixed) - data: OptionalFieldAliasModel = File(...), - ): + async def optional_field_alias_model(data: OptionalFieldAliasModel = Form()): if data.file is None: return {"file_size": None} return {"file_size": len(data.file)} @@ -85,17 +74,11 @@ def test_optional_field_alias_model_by_name(): assert resp.json() == {"file_size": None} -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_optional_field_alias_model_by_alias(): client = TestClient(app) resp = client.post("/optional-field-alias-model", files={"file_alias": b"content"}) assert resp.status_code == 200, resp.text - # Fails with: - # AssertionError: assert 422 == 200 - # {"detail":[{"loc":["body","file_alias"],"msg":"byte type expected","type":"type_error.bytes"}]} - - # Uncomment when the assertion above passes: - # assert resp.json() == {"file_size": 7} + assert resp.json() == {"file_size": 7} def test_optional_field_alias_model_schema(): @@ -116,13 +99,11 @@ def test_optional_field_alias_model_schema(): class ListFieldAliasModel(BaseModel): - files: List[bytes] = Field(alias="files_alias") + files: List[bytes] = File(alias="files_alias") @app.post("/list-field-alias-model") -async def list_field_alias_model( # pragma: no cover (remove `no cover` when bug fixed) - data: ListFieldAliasModel = File(...), -): +async def list_field_alias_model(data: ListFieldAliasModel = Form()): return {"file_sizes": [len(file) for file in data.files]} @@ -138,7 +119,6 @@ def test_list_field_alias_model_by_name(): assert "files_alias" in detail[0]["loc"] -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_list_field_alias_model_by_alias(): client = TestClient(app) resp = client.post( @@ -146,12 +126,7 @@ def test_list_field_alias_model_by_alias(): files=[("files_alias", b"content1"), ("files_alias", b"content2")], ) assert resp.status_code == 200, resp.text - # Fails with: - # AssertionError: assert 422 == 200 - # {"detail":[{"loc":["body","files_alias",0],"msg":"byte type expected","type":"type_error.bytes"},{"loc":["body","files_alias",1],"msg":"byte type expected","type":"type_error.bytes"}]} - - # Uncomment when the assertion above passes: - # assert resp.json() == {"file_sizes": [8, 8]} + assert resp.json() == {"file_sizes": [8, 8]} def test_list_field_alias_model_schema(): @@ -173,11 +148,11 @@ def test_list_field_alias_model_schema(): if not PYDANTIC_V2: class OptionalListFieldAliasModel(BaseModel): - files: Optional[List[bytes]] = Field(None, alias="files_alias", nullable=True) + files: Optional[List[bytes]] = File(None, alias="files_alias", nullable=True) @app.post("/optional-list-field-alias-model") - async def optional_list_field_alias_model( # pragma: no cover (remove `no cover` when bug fixed) - data: OptionalListFieldAliasModel = File(), + async def optional_list_field_alias_model( + data: OptionalListFieldAliasModel = Form(), ): if data.files is None: return {"file_sizes": None} @@ -194,7 +169,6 @@ def test_optional_list_field_alias_model_by_name(): assert resp.json() == {"file_sizes": None} -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_optional_list_field_alias_model_by_alias(): client = TestClient(app) resp = client.post( @@ -202,12 +176,7 @@ def test_optional_list_field_alias_model_by_alias(): files=[("files_alias", b"content1"), ("files_alias", b"content2")], ) assert resp.status_code == 200, resp.text - # Fails with: - # AssertionError: assert 422 == 200 - # {"detail":[{"loc":["body","files_alias",0],"msg":"byte type expected","type":"type_error.bytes"},{"loc":["body","files_alias",1],"msg":"byte type expected","type":"type_error.bytes"}]} - - # Uncomment when the assertion above passes: - # assert resp.json() == {"file_sizes": [8, 8]} + assert resp.json() == {"file_sizes": [8, 8]} def test_optional_list_field_alias_model_schema(): diff --git a/tests/test_aliases/test_form_field_aliases_pd_v1.py b/tests/test_aliases/test_form_field_aliases_pd_v1.py index d6402d8a0..e2ba4ed1f 100644 --- a/tests/test_aliases/test_form_field_aliases_pd_v1.py +++ b/tests/test_aliases/test_form_field_aliases_pd_v1.py @@ -1,7 +1,7 @@ from typing import List, Optional -import pytest from fastapi import FastAPI, Form +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from ..utils import needs_pydanticv1 @@ -13,8 +13,6 @@ app = FastAPI() # ===================================================================================== # Form(alias=...) # Current situation: Works -# Schema generation for optional field and optional list fails due to issue likely not related to aliases - # ------------------------------ # required field @@ -52,12 +50,13 @@ def test_required_field_alias_schema(): # ------------------------------ # optional field +if not PYDANTIC_V2: -@app.post("/optional-field-alias", operation_id="optional_field_alias") -async def optional_field_alias( - param: Optional[str] = Form(None, alias="param_alias"), -): - return {"param": param} + @app.post("/optional-field-alias", operation_id="optional_field_alias") + async def optional_field_alias( + param: Optional[str] = Form(None, alias="param_alias", nullable=True), + ): + return {"param": param} def test_optional_field_alias_by_name(): @@ -74,20 +73,16 @@ def test_optional_field_alias_by_alias(): assert resp.json() == {"param": "123"} -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_optional_field_alias_schema(): openapi = app.openapi() body_schema = openapi["components"]["schemas"]["Body_optional_field_alias"] assert body_schema["properties"] == { "param_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], + "type": "string", + "nullable": True, "title": "Param Alias", } } - # Fails with: - # AssertionError: assert - # {'param_alias': {'type': 'string', 'title': 'Param Alias'}} == - # {'param_alias': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Param Alias'}} # ------------------------------ @@ -130,12 +125,13 @@ def test_list_field_alias_schema(): # ------------------------------ # optional list field +if not PYDANTIC_V2: -@app.post("/optional-list-field-alias", operation_id="optional_list_field_alias") -async def optional_list_field_alias( - param: Optional[List[str]] = Form(None, alias="param_alias"), -): - return {"param": param} + @app.post("/optional-list-field-alias", operation_id="optional_list_field_alias") + async def optional_list_field_alias( + param: Optional[List[str]] = Form(None, alias="param_alias", nullable=True), + ): + return {"param": param} def test_optional_list_field_alias_by_name(): @@ -154,17 +150,14 @@ def test_optional_list_field_alias_by_alias(): assert resp.json() == {"param": ["123", "456"]} -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_optional_list_field_alias_schema(): openapi = app.openapi() body_schema = openapi["components"]["schemas"]["Body_optional_list_field_alias"] assert body_schema["properties"] == { "param_alias": { - "anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], + "items": {"type": "string"}, + "type": "array", + "nullable": True, "title": "Param Alias", }, } - # Fails with: - # AssertionError: assert - # {'param_alias': {'items': {'type': 'string'}, 'type': 'array', 'title': 'Param Alias'}} == - # {'param_alias': {'anyOf': [{'items': {'type': 'string'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Param Alias'}} diff --git a/tests/test_aliases/test_form_model_aliases_pd_v1.py b/tests/test_aliases/test_form_model_aliases_pd_v1.py index 4987a8d94..a939a31fb 100644 --- a/tests/test_aliases/test_form_model_aliases_pd_v1.py +++ b/tests/test_aliases/test_form_model_aliases_pd_v1.py @@ -1,7 +1,7 @@ from typing import List, Optional -import pytest from fastapi import FastAPI, Form +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel, Field @@ -14,7 +14,6 @@ app = FastAPI() # ===================================================================================== # Field(alias=...) # Current situation: works -# Schema generation for optional field and optional list fails due to issue likely not related to aliases # ------------------------------ # required field @@ -56,14 +55,14 @@ def test_required_field_alias_model_schema(): # ------------------------------ # optional field +if not PYDANTIC_V2: -class OptionalFieldAliasModel(BaseModel): - param: Optional[str] = Field(None, alias="param_alias") + class OptionalFieldAliasModel(BaseModel): + param: Optional[str] = Field(None, alias="param_alias", nullable=True) - -@app.post("/optional-field-alias-model") -async def optional_field_alias_model(data: OptionalFieldAliasModel = Form(...)): - return {"param": data.param} + @app.post("/optional-field-alias-model") + async def optional_field_alias_model(data: OptionalFieldAliasModel = Form(...)): + return {"param": data.param} def test_optional_field_alias_model_by_name(): @@ -80,20 +79,16 @@ def test_optional_field_alias_model_by_alias(): assert resp.json() == {"param": "123"} -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_optional_field_alias_model_schema(): openapi = app.openapi() body_schema = openapi["components"]["schemas"]["OptionalFieldAliasModel"] assert body_schema["properties"] == { "param_alias": { - "anyOf": [{"type": "string"}, {"type": "null"}], + "type": "string", + "nullable": True, "title": "Param Alias", }, } - # Fails with: - # AssertionError: assert - # {'param_alias': {'type': 'string', 'title': 'Param Alias'}} == - # {'param_alias': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Param Alias'}} # ------------------------------ @@ -140,16 +135,16 @@ def test_list_field_alias_model_schema(): # ------------------------------ # optional list field +if not PYDANTIC_V2: -class OptionalListFieldAliasModel(BaseModel): - param: Optional[List[str]] = Field(None, alias="param_alias") - + class OptionalListFieldAliasModel(BaseModel): + param: Optional[List[str]] = Field(None, alias="param_alias", nullable=True) -@app.post("/optional-list-field-alias-model") -async def optional_list_field_alias_model( - data: OptionalListFieldAliasModel = Form(...), -): - return {"param": data.param} + @app.post("/optional-list-field-alias-model") + async def optional_list_field_alias_model( + data: OptionalListFieldAliasModel = Form(...), + ): + return {"param": data.param} def test_optional_list_field_alias_model_by_name(): @@ -170,17 +165,14 @@ def test_optional_list_field_alias_model_by_alias(): assert resp.json() == {"param": ["123", "456"]} -@pytest.mark.xfail(raises=AssertionError, strict=False) def test_optional_list_field_alias_model_schema(): openapi = app.openapi() body_schema = openapi["components"]["schemas"]["OptionalListFieldAliasModel"] assert body_schema["properties"] == { "param_alias": { - "anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], + "items": {"type": "string"}, + "type": "array", + "nullable": True, "title": "Param Alias", } } - # Fails with: - # AssertionError: assert - # {'param_alias': {'items': {'type': 'string'}, 'type': 'array', 'title': 'Param Alias'}} == - # {'param_alias': {'anyOf': [{'items': {'type': 'string'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Param Alias'}} diff --git a/tests/test_aliases/test_uploadfile_model_aliases_pd_v1.py b/tests/test_aliases/test_uploadfile_model_aliases_pd_v1.py index 780b5716a..de3adad08 100644 --- a/tests/test_aliases/test_uploadfile_model_aliases_pd_v1.py +++ b/tests/test_aliases/test_uploadfile_model_aliases_pd_v1.py @@ -1,9 +1,9 @@ from typing import List, Optional -from fastapi import FastAPI, File, UploadFile +from fastapi import FastAPI, File, Form, UploadFile from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel, Field +from pydantic import BaseModel from ..utils import needs_pydanticv1 @@ -20,11 +20,11 @@ app = FastAPI() class RequiredFieldAliasModel(BaseModel): - file: UploadFile = Field(alias="file_alias") + file: UploadFile = File(alias="file_alias") @app.post("/required-field-alias-model") -async def required_field_alias_model(data: RequiredFieldAliasModel = File(...)): +async def required_field_alias_model(data: RequiredFieldAliasModel = Form()): return {"file_size": data.file.size} @@ -58,10 +58,10 @@ def test_required_field_alias_model_schema(): if not PYDANTIC_V2: class OptionalFieldAliasModel(BaseModel): - file: Optional[UploadFile] = Field(None, alias="file_alias", nullable=True) + file: Optional[UploadFile] = File(None, alias="file_alias", nullable=True) @app.post("/optional-field-alias-model") - async def optional_field_alias_model(data: OptionalFieldAliasModel = File(...)): + async def optional_field_alias_model(data: OptionalFieldAliasModel = Form()): if data.file is None: return {"file_size": None} return {"file_size": data.file.size} @@ -99,11 +99,11 @@ def test_optional_field_alias_model_schema(): class ListFieldAliasModel(BaseModel): - files: List[UploadFile] = Field(alias="files_alias") + files: List[UploadFile] = File(alias="files_alias") @app.post("/list-field-alias-model") -async def list_field_alias_model(data: ListFieldAliasModel = File(...)): +async def list_field_alias_model(data: ListFieldAliasModel = Form()): return {"file_sizes": [file.size for file in data.files]} @@ -147,13 +147,13 @@ def test_list_field_alias_model_schema(): if not PYDANTIC_V2: class OptionalListFieldAliasModel(BaseModel): - files: Optional[List[UploadFile]] = Field( + files: Optional[List[UploadFile]] = File( None, alias="files_alias", nullable=True ) @app.post("/optional-list-field-alias-model") async def optional_list_field_alias_model( - data: OptionalListFieldAliasModel = File(), + data: OptionalListFieldAliasModel = Form(), ): if data.files is None: return {"file_sizes": None}