Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V1 models for response and body #10223

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
70 changes: 54 additions & 16 deletions fastapi/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")


sequence_annotation_to_type = {
Sequence: list,
List: list,
Expand Down Expand Up @@ -98,9 +97,14 @@ def type_(self) -> Any:
return self.field_info.annotation

def __post_init__(self) -> None:
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[self.field_info.annotation, self.field_info]
)
from pydantic import PydanticDeprecatedSince20

try:
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[self.field_info.annotation, self.field_info]
)
except PydanticDeprecatedSince20:
pass

def get_default(self) -> Any:
if self.field_info.is_required():
Expand All @@ -123,6 +127,16 @@ def validate(
return None, _regenerate_error_with_loc(
errors=exc.errors(), loc_prefix=loc
)
except AttributeError:
# pydantic v1
from pydantic import v1

try:
return v1.parse_obj_as(self.type_, value), None
except v1.ValidationError as exc:
return None, _regenerate_error_with_loc(
errors=exc.errors(), loc_prefix=loc
)

def serialize(
self,
Expand All @@ -136,18 +150,42 @@ def serialize(
exclude_defaults: bool = False,
exclude_none: bool = False,
) -> Any:
# What calls this code passes a value that already called
# self._type_adapter.validate_python(value)
return self._type_adapter.dump_python(
value,
mode=mode,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
try:
# What calls this code passes a value that already called
# self._type_adapter.validate_python(value)
return self._type_adapter.dump_python(
value,
mode=mode,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
except AttributeError:
# pydantic v1
try:
return value.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
except AttributeError:
return [
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if there is a better way to handle the case where there is a container of model instances.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isinstance()?

if isinstance(value, dict):
    ...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for Python 3.11+, I expect its zero-overhead exception handling to be a bit more efficient. In case that's neglectable, an if/else branch with isinstance might be more readable.

item.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
for item in value
]

def __hash__(self) -> int:
# Each ModelField is unique for our purposes, to allow making a dict from
Expand Down
99 changes: 99 additions & 0 deletions tests/test_pydantic_v1_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from typing import List, Optional

import pytest
from fastapi import Body, FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
from typing_extensions import Annotated

from tests.utils import needs_pydanticv2

if PYDANTIC_V2:
from pydantic import v1

class Item(v1.BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: list = []

class Model(v1.BaseModel):
name: str

else:
from pydantic import BaseModel

class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: list = []

class Model(BaseModel):
name: str


app = FastAPI()


@app.post("/request_body")
async def request_body(body: Annotated[Item, Body()]):
return body


@app.get("/response_model", response_model=Model)
async def response_model():
return Model(name="valid_model")


@app.get("/response_model__invalid", response_model=Model)
async def response_model__invalid():
return 1


@app.get("/response_model_list", response_model=List[Model])
async def response_model_list():
return [Model(name="valid_model")]


@app.get("/response_model_list__invalid", response_model=List[Model])
async def response_model_list__invalid():
return [1]


client = TestClient(app)


@needs_pydanticv2
class TestResponseModel:
def test_simple__valid(self):
response = client.get("/response_model")
assert response.status_code == 200
assert response.json() == {"name": "valid_model"}

def test_simple__invalid(self):
with pytest.raises(ResponseValidationError):
client.get("/response_model__invalid")

def test_list__valid(self):
response = client.get("/response_model_list")
assert response.status_code == 200
assert response.json() == [{"name": "valid_model"}]

def test_list__invalid(self):
with pytest.raises(ResponseValidationError):
client.get("/response_model_list__invalid")


@needs_pydanticv2
class TestRequestBody:
def test_model__valid(self):
response = client.post("/request_body", json={"name": "myname", "price": 1.0})
assert response.status_code == 200, response.text

def test_model__invalid(self):
response = client.post("/request_body", json={"name": "myname"})
assert response.status_code == 422, response.text