Browse Source

Merge e774a05e32 into 460f8d2cc8

pull/15329/merge
Yuta Hayashibe 15 hours ago
committed by GitHub
parent
commit
31e82500ec
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 22
      fastapi/_compat/v2.py
  2. 66
      tests/test_json_field_serialize_as_any.py

22
fastapi/_compat/v2.py

@ -392,14 +392,28 @@ def create_body_model(
return BodyModel return BodyModel
def _needs_no_config(type_: Any) -> bool:
"""Return True if TypeAdapter must not receive config= for this type.
Pydantic raises PydanticUserError when config= is passed to TypeAdapter and
the resolved type (or its inner type) is a BaseModel, dataclass, or TypedDict.
We therefore suppress config= not only for bare BaseModel/dataclass types but
also for Annotated wrappers whose first argument is such a type this covers
Json[BaseModel], Json[SerializeAsAny[BaseModel]], and plain
Annotated[BaseModel, ...] forms.
"""
if lenient_issubclass(type_, (BaseModel, dict)) or is_dataclass(type_):
return True
if get_origin(type_) is Annotated:
return _needs_no_config(get_args(type_)[0])
return False
def get_model_fields(model: type[BaseModel]) -> list[ModelField]: def get_model_fields(model: type[BaseModel]) -> list[ModelField]:
model_fields: list[ModelField] = [] model_fields: list[ModelField] = []
for name, field_info in model.model_fields.items(): for name, field_info in model.model_fields.items():
type_ = field_info.annotation type_ = field_info.annotation
if lenient_issubclass(type_, (BaseModel, dict)) or is_dataclass(type_): model_config = None if _needs_no_config(type_) else model.model_config
model_config = None
else:
model_config = model.model_config
model_fields.append( model_fields.append(
ModelField( ModelField(
field_info=field_info, field_info=field_info,

66
tests/test_json_field_serialize_as_any.py

@ -0,0 +1,66 @@
"""
Regression tests for:
PydanticUserError when a generic BaseModel with a Json[T] field is
specialized with SerializeAsAny[SomeModel].
fastapi/_compat/v2.py: get_model_fields() must suppress config= for
Annotated-wrapped types whose inner type is a BaseModel / dataclass,
not only for bare BaseModel types.
"""
from typing import Generic, TypeVar
from fastapi import FastAPI
from pydantic import BaseModel, Json, SerializeAsAny
T = TypeVar("T")
class Inner(BaseModel):
value: str
class Source(BaseModel, Generic[T]):
payload: Json[T]
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_openapi_schema_generation_serialize_as_any_does_not_raise():
"""app.openapi() must not raise PydanticUserError for Source[SerializeAsAny[Inner]].
Previously raised:
pydantic.errors.PydanticUserError: Cannot use `config` when the type
is a BaseModel, dataclass or TypedDict.
Root cause: get_model_fields() in fastapi/_compat/v2.py did not unwrap
Annotated before checking if config= should be suppressed, so
Json[Annotated[Inner, SerializeAsAny]] slipped through and TypeAdapter
received config= illegally.
"""
app = FastAPI()
@app.get("/search")
def search() -> Source[SerializeAsAny[Inner]]: # pragma: no cover
...
schema = app.openapi()
assert schema is not None
# The route must appear in the generated schema.
assert "/search" in schema["paths"]
def test_openapi_schema_generation_plain_inner_unaffected():
"""Baseline: Source[Inner] (no SerializeAsAny) must continue to work."""
app = FastAPI()
@app.get("/search")
def search() -> Source[Inner]: # pragma: no cover
...
schema = app.openapi()
assert schema is not None
assert "/search" in schema["paths"]
Loading…
Cancel
Save