You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
5.7 KiB

from collections.abc import Iterator
import pytest
from fastapi import FastAPI
from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel
class User(BaseModel):
name: str
class UserLogin(User):
password: str
class OuterModel(BaseModel):
user: User
class Item(BaseModel):
name: str
description: str
class ItemWithPrice(Item):
price: float
requires_pydantic_v2_13 = pytest.mark.skipif(
PYDANTIC_VERSION_MINOR_TUPLE < (2, 13),
reason="polymorphic_serialization requires Pydantic >= 2.13",
)
only_for_pydantic_below_v2_13 = pytest.mark.skipif(
PYDANTIC_VERSION_MINOR_TUPLE >= (2, 13),
reason="This test only applies to Pydantic < 2.13",
)
requires_pydantic_v2_13 = pytest.mark.skipif(
PYDANTIC_VERSION_MINOR_TUPLE < (2, 13),
reason="polymorphic_serialization requires Pydantic >= 2.13",
)
only_for_pydantic_below_v2_13 = pytest.mark.skipif(
PYDANTIC_VERSION_MINOR_TUPLE >= (2, 13),
reason="This test only applies to Pydantic < 2.13",
)
def test_default_behavior_filters_subclass_fields():
app = FastAPI()
@app.get("/user", response_model=OuterModel)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic"}}
@requires_pydantic_v2_13
def test_polymorphic_flag_includes_subclass_fields():
app = FastAPI()
@app.get(
"/user",
response_model=OuterModel,
response_model_polymorphic_serialization=True,
)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
@requires_pydantic_v2_13
def test_polymorphic_with_inferred_response_model():
app = FastAPI()
@app.get("/user", response_model_polymorphic_serialization=True)
def get_user() -> OuterModel:
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
@requires_pydantic_v2_13
def test_polymorphic_with_fast_json_path():
app = FastAPI()
@app.post(
"/item",
response_model=Item,
response_model_polymorphic_serialization=True,
status_code=201,
)
def create_item() -> Item:
return ItemWithPrice(name="Widget", description="A useful widget", price=9.99)
response = TestClient(app).post("/item")
assert response.status_code == 201
assert response.json() == {
"name": "Widget",
"description": "A useful widget",
"price": 9.99,
}
@requires_pydantic_v2_13
def test_polymorphic_with_custom_response_class():
app = FastAPI()
class CustomJSONResponse(JSONResponse):
media_type = "application/json"
@app.get(
"/user",
response_model=OuterModel,
response_model_polymorphic_serialization=True,
response_class=CustomJSONResponse,
)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
@requires_pydantic_v2_13
def test_polymorphic_composes_with_exclude():
app = FastAPI()
@app.get(
"/user",
response_model=OuterModel,
response_model_polymorphic_serialization=True,
response_model_exclude={"user": {"password"}},
)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
response = TestClient(app).get("/user")
assert response.status_code == 200
assert response.json() == {"user": {"name": "pydantic"}}
def test_list_response_filters_subclass_fields_by_default():
app = FastAPI()
def generate_items() -> Iterator[Item]:
yield ItemWithPrice(name="Item1", description="First item", price=10.0)
@app.get("/items/stream", response_model=list[Item])
def stream_items():
return list(generate_items())
response = TestClient(app).get("/items/stream")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0] == {"name": "Item1", "description": "First item"}
@only_for_pydantic_below_v2_13
def test_raises_error_for_unsupported_pydantic_version():
app = FastAPI()
@app.get(
"/user",
response_model=OuterModel,
response_model_polymorphic_serialization=True,
)
def get_user():
return OuterModel(user=UserLogin(name="pydantic", password="password"))
with pytest.raises(ValueError) as exc_info:
TestClient(app).get("/user")
assert "polymorphic_serialization requires Pydantic >= 2.13" in str(exc_info.value)
def test_flag_defaults_to_false():
app = FastAPI()
@app.get("/user1", response_model=OuterModel)
def get_user1():
return OuterModel(user=UserLogin(name="user1", password="pass1"))
@app.post("/user2", response_model=OuterModel)
def post_user2():
return OuterModel(user=UserLogin(name="user2", password="pass2"))
client = TestClient(app)
response1 = client.get("/user1")
assert response1.json() == {"user": {"name": "user1"}}
response2 = client.post("/user2")
assert response2.json() == {"user": {"name": "user2"}}