]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Add support for `from pydantic.v1 import BaseModel`, mixed Pydantic v1 and v2 model...
authorSebastián Ramírez <tiangolo@gmail.com>
Sat, 11 Oct 2025 16:45:54 +0000 (18:45 +0200)
committerGitHub <noreply@github.com>
Sat, 11 Oct 2025 16:45:54 +0000 (18:45 +0200)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
46 files changed:
docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md [new file with mode: 0644]
docs/en/mkdocs.yml
docs_src/pydantic_v1_in_v2/tutorial001_an.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/tutorial002_an.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/tutorial003_an.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/tutorial004_an.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py [new file with mode: 0644]
fastapi/_compat.py [deleted file]
fastapi/_compat/__init__.py [new file with mode: 0644]
fastapi/_compat/main.py [new file with mode: 0644]
fastapi/_compat/model_field.py [new file with mode: 0644]
fastapi/_compat/shared.py [new file with mode: 0644]
fastapi/_compat/v1.py [new file with mode: 0644]
fastapi/_compat/v2.py [new file with mode: 0644]
fastapi/datastructures.py
fastapi/dependencies/utils.py
fastapi/encoders.py
fastapi/openapi/utils.py
fastapi/routing.py
fastapi/temp_pydantic_v1_params.py [new file with mode: 0644]
fastapi/utils.py
tests/test_compat.py
tests/test_compat_params_v1.py [new file with mode: 0644]
tests/test_get_model_definitions_formfeed_escape.py
tests/test_openapi_separate_input_output_schemas.py
tests/test_pydantic_v1_v2_01.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_list.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_mixed.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_multifile/__init__.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_multifile/main.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_multifile/modelsv1.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_multifile/modelsv2.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_multifile/modelsv2b.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_multifile/test_multifile.py [new file with mode: 0644]
tests/test_pydantic_v1_v2_noneable.py [new file with mode: 0644]
tests/test_response_model_as_return_annotation.py
tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py [new file with mode: 0644]
tests/utils.py

diff --git a/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md b/docs/en/docs/how-to/migrate-from-pydantic-v1-to-pydantic-v2.md
new file mode 100644 (file)
index 0000000..e85d122
--- /dev/null
@@ -0,0 +1,133 @@
+# Migrate from Pydantic v1 to Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 }
+
+If you have an old FastAPI app, you might be using Pydantic version 1.
+
+FastAPI has had support for either Pydantic v1 or v2 since version 0.100.0.
+
+If you had installed Pydantic v2, it would use it. If instead you had Pydantic v1, it would use that.
+
+Pydantic v1 is now deprecated and support for it will be removed in the next versions of FastAPI, you should **migrate to Pydantic v2**. This way you will get the latest features, improvements, and fixes.
+
+/// warning
+
+Also, the Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**.
+
+If you want to use the latest features of Python, you will need to make sure you use Pydantic v2.
+
+///
+
+If you have an old FastAPI app with Pydantic v1, here I'll show you how to migrate it to Pydantic v2, and the **new features in FastAPI 0.119.0** to help you with a gradual migration.
+
+## Official Guide { #official-guide }
+
+Pydantic has an official <a href="https://docs.pydantic.dev/latest/migration/" class="external-link" target="_blank">Migration Guide</a> from v1 to v2.
+
+It also includes what has changed, how validations are now more correct and strict, possible caveats, etc.
+
+You can read it to understand better what has changed.
+
+## Tests { #tests }
+
+Make sure you have [tests](../tutorial/testing.md){.internal-link target=_blank} for your app and you run them on continuous integration (CI).
+
+This way, you can do the upgrade and make sure everything is still working as expected.
+
+## `bump-pydantic` { #bump-pydantic }
+
+In many cases, when you use regular Pydantic models without customizations, you will be able to automate most of the process of migrating from Pydantic v1 to Pydantic v2.
+
+You can use <a href="https://github.com/pydantic/bump-pydantic" class="external-link" target="_blank">`bump-pydantic`</a> from the same Pydantic team.
+
+This tool will help you to automatically change most of the code that needs to be changed.
+
+After this, you can run the tests and check if everything works. If it does, you are done. 😎
+
+## Pydantic v1 in v2 { #pydantic-v1-in-v2 }
+
+Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`.
+
+This means that you can install the latest version of Pydantic v2 and import and use the old Pydantic v1 components from this submodule, as if you had the old Pydantic v1 installed.
+
+{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *}
+
+### FastAPI support for Pydantic v1 in v2 { #fastapi-support-for-pydantic-v1-in-v2 }
+
+Since FastAPI 0.119.0, there's also partial support for Pydantic v1 from inside of Pydantic v2, to facilitate the migration to v2.
+
+So, you could upgrade Pydantic to the latest version 2, and change the imports to use the `pydantic.v1` submodule, and in many cases it would just work.
+
+{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *}
+
+/// warning
+
+Have in mind that as the Pydantic team no longer supports Pydantic v1 in recent versions of Python, starting from Python 3.14, using `pydantic.v1` is also not supported in Python 3.14 and above.
+
+///
+
+### Pydantic v1 and v2 on the same app { #pydantic-v1-and-v2-on-the-same-app }
+
+It's **not supported** by Pydantic to have a model of Pydantic v2 with its own fields defined as Pydantic v1 models or vice versa.
+
+```mermaid
+graph TB
+    subgraph "❌ Not Supported"
+        direction TB
+        subgraph V2["Pydantic v2 Model"]
+            V1Field["Pydantic v1 Model"]
+        end
+        subgraph V1["Pydantic v1 Model"]
+            V2Field["Pydantic v2 Model"]
+        end
+    end
+
+    style V2 fill:#f9fff3
+    style V1 fill:#fff6f0
+    style V1Field fill:#fff6f0
+    style V2Field fill:#f9fff3
+```
+
+...but, you can have separated models using Pydantic v1 and v2 in the same app.
+
+```mermaid
+graph TB
+    subgraph "✅ Supported"
+        direction TB
+        subgraph V2["Pydantic v2 Model"]
+            V2Field["Pydantic v2 Model"]
+        end
+        subgraph V1["Pydantic v1 Model"]
+            V1Field["Pydantic v1 Model"]
+        end
+    end
+
+    style V2 fill:#f9fff3
+    style V1 fill:#fff6f0
+    style V1Field fill:#fff6f0
+    style V2Field fill:#f9fff3
+```
+
+In some cases, it's even possible to have both Pydantic v1 and v2 models in the same **path operation** in your FastAPI app:
+
+{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *}
+
+In this example above, the input model is a Pydantic v1 model, and the output model (defined in `response_model=ItemV2`) is a Pydantic v2 model.
+
+### Pydantic v1 parameters { #pydantic-v1-parameters }
+
+If you need to use some of the FastAPI-specific tools for parameters like `Body`, `Query`, `Form`, etc. with Pydantic v1 models, you can import them from `fastapi.temp_pydantic_v1_params` while you finish the migration to Pydantic v2:
+
+{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *}
+
+### Migrate in steps { #migrate-in-steps }
+
+/// tip
+
+First try with `bump-pydantic`, if your tests pass and that works, then you're done in one command. ✨
+
+///
+
+If `bump-pydantic` doesn't work for your use case, you can use the support for both Pydantic v1 and v2 models in the same app to do the migration to Pydantic v2 gradually.
+
+You could fist upgrade Pydantic to use the latest version 2, and change the imports to use `pydantic.v1` for all your models.
+
+Then, you can start migrating your models from Pydantic v1 to v2 in groups, in gradual steps. 🚶
index e85f311021c605d6d12628d8f0057e9f28111e70..323035240a1b277ed784668bd3e02ae19e4763ea 100644 (file)
@@ -201,6 +201,7 @@ nav:
   - How To - Recipes:
     - how-to/index.md
     - how-to/general.md
+    - how-to/migrate-from-pydantic-v1-to-pydantic-v2.md
     - how-to/graphql.md
     - how-to/custom-request-and-route.md
     - how-to/conditional-openapi.md
diff --git a/docs_src/pydantic_v1_in_v2/tutorial001_an.py b/docs_src/pydantic_v1_in_v2/tutorial001_an.py
new file mode 100644 (file)
index 0000000..62a4b2c
--- /dev/null
@@ -0,0 +1,9 @@
+from typing import Union
+
+from pydantic.v1 import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    size: float
diff --git a/docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py
new file mode 100644 (file)
index 0000000..a8ec729
--- /dev/null
@@ -0,0 +1,7 @@
+from pydantic.v1 import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    description: str | None = None
+    size: float
diff --git a/docs_src/pydantic_v1_in_v2/tutorial002_an.py b/docs_src/pydantic_v1_in_v2/tutorial002_an.py
new file mode 100644 (file)
index 0000000..3c6a060
--- /dev/null
@@ -0,0 +1,18 @@
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic.v1 import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    size: float
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+async def create_item(item: Item) -> Item:
+    return item
diff --git a/docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py
new file mode 100644 (file)
index 0000000..4934e70
--- /dev/null
@@ -0,0 +1,16 @@
+from fastapi import FastAPI
+from pydantic.v1 import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    description: str | None = None
+    size: float
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+async def create_item(item: Item) -> Item:
+    return item
diff --git a/docs_src/pydantic_v1_in_v2/tutorial003_an.py b/docs_src/pydantic_v1_in_v2/tutorial003_an.py
new file mode 100644 (file)
index 0000000..117d6f7
--- /dev/null
@@ -0,0 +1,25 @@
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel as BaseModelV2
+from pydantic.v1 import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    size: float
+
+
+class ItemV2(BaseModelV2):
+    name: str
+    description: Union[str, None] = None
+    size: float
+
+
+app = FastAPI()
+
+
+@app.post("/items/", response_model=ItemV2)
+async def create_item(item: Item):
+    return item
diff --git a/docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py
new file mode 100644 (file)
index 0000000..6e30136
--- /dev/null
@@ -0,0 +1,23 @@
+from fastapi import FastAPI
+from pydantic import BaseModel as BaseModelV2
+from pydantic.v1 import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    description: str | None = None
+    size: float
+
+
+class ItemV2(BaseModelV2):
+    name: str
+    description: str | None = None
+    size: float
+
+
+app = FastAPI()
+
+
+@app.post("/items/", response_model=ItemV2)
+async def create_item(item: Item):
+    return item
diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an.py b/docs_src/pydantic_v1_in_v2/tutorial004_an.py
new file mode 100644 (file)
index 0000000..cca8a9e
--- /dev/null
@@ -0,0 +1,20 @@
+from typing import Union
+
+from fastapi import FastAPI
+from fastapi.temp_pydantic_v1_params import Body
+from pydantic.v1 import BaseModel
+from typing_extensions import Annotated
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    size: float
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item:
+    return item
diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py b/docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py
new file mode 100644 (file)
index 0000000..c251311
--- /dev/null
@@ -0,0 +1,19 @@
+from typing import Annotated
+
+from fastapi import FastAPI
+from fastapi.temp_pydantic_v1_params import Body
+from pydantic.v1 import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    description: str | None = None
+    size: float
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item:
+    return item
diff --git a/docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py b/docs_src/pydantic_v1_in_v2/tutorial004_an_py39.py
new file mode 100644 (file)
index 0000000..150ab20
--- /dev/null
@@ -0,0 +1,19 @@
+from typing import Annotated, Union
+
+from fastapi import FastAPI
+from fastapi.temp_pydantic_v1_params import Body
+from pydantic.v1 import BaseModel
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    size: float
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+async def create_item(item: Annotated[Item, Body(embed=True)]) -> Item:
+    return item
diff --git a/fastapi/_compat.py b/fastapi/_compat.py
deleted file mode 100644 (file)
index 21ea1a2..0000000
+++ /dev/null
@@ -1,680 +0,0 @@
-import warnings
-from collections import deque
-from copy import copy
-from dataclasses import dataclass, is_dataclass
-from enum import Enum
-from functools import lru_cache
-from typing import (
-    Any,
-    Callable,
-    Deque,
-    Dict,
-    FrozenSet,
-    List,
-    Mapping,
-    Sequence,
-    Set,
-    Tuple,
-    Type,
-    Union,
-    cast,
-)
-
-from fastapi.exceptions import RequestErrorModel
-from fastapi.types import IncEx, ModelNameMap, UnionType
-from pydantic import BaseModel, create_model
-from pydantic.version import VERSION as PYDANTIC_VERSION
-from starlette.datastructures import UploadFile
-from typing_extensions import Annotated, Literal, get_args, get_origin
-
-PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2])
-PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
-
-
-sequence_annotation_to_type = {
-    Sequence: list,
-    List: list,
-    list: list,
-    Tuple: tuple,
-    tuple: tuple,
-    Set: set,
-    set: set,
-    FrozenSet: frozenset,
-    frozenset: frozenset,
-    Deque: deque,
-    deque: deque,
-}
-
-sequence_types = tuple(sequence_annotation_to_type.keys())
-
-Url: Type[Any]
-
-if PYDANTIC_V2:
-    from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
-    from pydantic import TypeAdapter
-    from pydantic import ValidationError as ValidationError
-    from pydantic._internal._schema_generation_shared import (  # type: ignore[attr-defined]
-        GetJsonSchemaHandler as GetJsonSchemaHandler,
-    )
-    from pydantic._internal._typing_extra import eval_type_lenient
-    from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
-    from pydantic.fields import FieldInfo
-    from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
-    from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
-    from pydantic_core import CoreSchema as CoreSchema
-    from pydantic_core import PydanticUndefined, PydanticUndefinedType
-    from pydantic_core import Url as Url
-
-    try:
-        from pydantic_core.core_schema import (
-            with_info_plain_validator_function as with_info_plain_validator_function,
-        )
-    except ImportError:  # pragma: no cover
-        from pydantic_core.core_schema import (
-            general_plain_validator_function as with_info_plain_validator_function,  # noqa: F401
-        )
-
-    RequiredParam = PydanticUndefined
-    Undefined = PydanticUndefined
-    UndefinedType = PydanticUndefinedType
-    evaluate_forwardref = eval_type_lenient
-    Validator = Any
-
-    class BaseConfig:
-        pass
-
-    class ErrorWrapper(Exception):
-        pass
-
-    @dataclass
-    class ModelField:
-        field_info: FieldInfo
-        name: str
-        mode: Literal["validation", "serialization"] = "validation"
-
-        @property
-        def alias(self) -> str:
-            a = self.field_info.alias
-            return a if a is not None else self.name
-
-        @property
-        def required(self) -> bool:
-            return self.field_info.is_required()
-
-        @property
-        def default(self) -> Any:
-            return self.get_default()
-
-        @property
-        def type_(self) -> Any:
-            return self.field_info.annotation
-
-        def __post_init__(self) -> None:
-            with warnings.catch_warnings():
-                # Pydantic >= 2.12.0 warns about field specific metadata that is unused
-                # (e.g. `TypeAdapter(Annotated[int, Field(alias='b')])`). In some cases, we
-                # end up building the type adapter from a model field annotation so we
-                # need to ignore the warning:
-                if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 12):
-                    from pydantic.warnings import UnsupportedFieldAttributeWarning
-
-                    warnings.simplefilter(
-                        "ignore", category=UnsupportedFieldAttributeWarning
-                    )
-                self._type_adapter: TypeAdapter[Any] = TypeAdapter(
-                    Annotated[self.field_info.annotation, self.field_info]
-                )
-
-        def get_default(self) -> Any:
-            if self.field_info.is_required():
-                return Undefined
-            return self.field_info.get_default(call_default_factory=True)
-
-        def validate(
-            self,
-            value: Any,
-            values: Dict[str, Any] = {},  # noqa: B006
-            *,
-            loc: Tuple[Union[int, str], ...] = (),
-        ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
-            try:
-                return (
-                    self._type_adapter.validate_python(value, from_attributes=True),
-                    None,
-                )
-            except ValidationError as exc:
-                return None, _regenerate_error_with_loc(
-                    errors=exc.errors(include_url=False), loc_prefix=loc
-                )
-
-        def serialize(
-            self,
-            value: Any,
-            *,
-            mode: Literal["json", "python"] = "json",
-            include: Union[IncEx, None] = None,
-            exclude: Union[IncEx, None] = None,
-            by_alias: bool = True,
-            exclude_unset: bool = False,
-            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,
-            )
-
-        def __hash__(self) -> int:
-            # Each ModelField is unique for our purposes, to allow making a dict from
-            # ModelField to its JSON Schema.
-            return id(self)
-
-    def get_annotation_from_field_info(
-        annotation: Any, field_info: FieldInfo, field_name: str
-    ) -> Any:
-        return annotation
-
-    def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
-        return errors  # type: ignore[return-value]
-
-    def _model_rebuild(model: Type[BaseModel]) -> None:
-        model.model_rebuild()
-
-    def _model_dump(
-        model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
-    ) -> Any:
-        return model.model_dump(mode=mode, **kwargs)
-
-    def _get_model_config(model: BaseModel) -> Any:
-        return model.model_config
-
-    def get_schema_from_model_field(
-        *,
-        field: ModelField,
-        schema_generator: GenerateJsonSchema,
-        model_name_map: ModelNameMap,
-        field_mapping: Dict[
-            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
-        ],
-        separate_input_output_schemas: bool = True,
-    ) -> Dict[str, Any]:
-        override_mode: Union[Literal["validation"], None] = (
-            None if separate_input_output_schemas else "validation"
-        )
-        # This expects that GenerateJsonSchema was already used to generate the definitions
-        json_schema = field_mapping[(field, override_mode or field.mode)]
-        if "$ref" not in json_schema:
-            # TODO remove when deprecating Pydantic v1
-            # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
-            json_schema["title"] = (
-                field.field_info.title or field.alias.title().replace("_", " ")
-            )
-        return json_schema
-
-    def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
-        return {}
-
-    def get_definitions(
-        *,
-        fields: List[ModelField],
-        schema_generator: GenerateJsonSchema,
-        model_name_map: ModelNameMap,
-        separate_input_output_schemas: bool = True,
-    ) -> Tuple[
-        Dict[
-            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
-        ],
-        Dict[str, Dict[str, Any]],
-    ]:
-        override_mode: Union[Literal["validation"], None] = (
-            None if separate_input_output_schemas else "validation"
-        )
-        inputs = [
-            (field, override_mode or field.mode, field._type_adapter.core_schema)
-            for field in fields
-        ]
-        field_mapping, definitions = schema_generator.generate_definitions(
-            inputs=inputs
-        )
-        for item_def in cast(Dict[str, Dict[str, Any]], definitions).values():
-            if "description" in item_def:
-                item_description = cast(str, item_def["description"]).split("\f")[0]
-                item_def["description"] = item_description
-        return field_mapping, definitions  # type: ignore[return-value]
-
-    def is_scalar_field(field: ModelField) -> bool:
-        from fastapi import params
-
-        return field_annotation_is_scalar(
-            field.field_info.annotation
-        ) and not isinstance(field.field_info, params.Body)
-
-    def is_sequence_field(field: ModelField) -> bool:
-        return field_annotation_is_sequence(field.field_info.annotation)
-
-    def is_scalar_sequence_field(field: ModelField) -> bool:
-        return field_annotation_is_scalar_sequence(field.field_info.annotation)
-
-    def is_bytes_field(field: ModelField) -> bool:
-        return is_bytes_or_nonable_bytes_annotation(field.type_)
-
-    def is_bytes_sequence_field(field: ModelField) -> bool:
-        return is_bytes_sequence_annotation(field.type_)
-
-    def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
-        cls = type(field_info)
-        merged_field_info = cls.from_annotation(annotation)
-        new_field_info = copy(field_info)
-        new_field_info.metadata = merged_field_info.metadata
-        new_field_info.annotation = merged_field_info.annotation
-        return new_field_info
-
-    def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
-        origin_type = (
-            get_origin(field.field_info.annotation) or field.field_info.annotation
-        )
-        assert issubclass(origin_type, sequence_types)  # type: ignore[arg-type]
-        return sequence_annotation_to_type[origin_type](value)  # type: ignore[no-any-return]
-
-    def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
-        error = ValidationError.from_exception_data(
-            "Field required", [{"type": "missing", "loc": loc, "input": {}}]
-        ).errors(include_url=False)[0]
-        error["input"] = None
-        return error  # type: ignore[return-value]
-
-    def create_body_model(
-        *, fields: Sequence[ModelField], model_name: str
-    ) -> Type[BaseModel]:
-        field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
-        BodyModel: Type[BaseModel] = create_model(model_name, **field_params)  # type: ignore[call-overload]
-        return BodyModel
-
-    def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
-        return [
-            ModelField(field_info=field_info, name=name)
-            for name, field_info in model.model_fields.items()
-        ]
-
-else:
-    from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
-    from pydantic import AnyUrl as Url  # noqa: F401
-    from pydantic import (  # type: ignore[assignment]
-        BaseConfig as BaseConfig,  # noqa: F401
-    )
-    from pydantic import ValidationError as ValidationError  # noqa: F401
-    from pydantic.class_validators import (  # type: ignore[no-redef]
-        Validator as Validator,  # noqa: F401
-    )
-    from pydantic.error_wrappers import (  # type: ignore[no-redef]
-        ErrorWrapper as ErrorWrapper,  # noqa: F401
-    )
-    from pydantic.errors import MissingError
-    from pydantic.fields import (  # type: ignore[attr-defined]
-        SHAPE_FROZENSET,
-        SHAPE_LIST,
-        SHAPE_SEQUENCE,
-        SHAPE_SET,
-        SHAPE_SINGLETON,
-        SHAPE_TUPLE,
-        SHAPE_TUPLE_ELLIPSIS,
-    )
-    from pydantic.fields import FieldInfo as FieldInfo
-    from pydantic.fields import (  # type: ignore[no-redef,attr-defined]
-        ModelField as ModelField,  # noqa: F401
-    )
-
-    # Keeping old "Required" functionality from Pydantic V1, without
-    # shadowing typing.Required.
-    RequiredParam: Any = Ellipsis  # type: ignore[no-redef]
-    from pydantic.fields import (  # type: ignore[no-redef,attr-defined]
-        Undefined as Undefined,
-    )
-    from pydantic.fields import (  # type: ignore[no-redef, attr-defined]
-        UndefinedType as UndefinedType,  # noqa: F401
-    )
-    from pydantic.schema import (
-        field_schema,
-        get_flat_models_from_fields,
-        get_model_name_map,
-        model_process_schema,
-    )
-    from pydantic.schema import (  # type: ignore[no-redef]  # noqa: F401
-        get_annotation_from_field_info as get_annotation_from_field_info,
-    )
-    from pydantic.typing import (  # type: ignore[no-redef]
-        evaluate_forwardref as evaluate_forwardref,  # noqa: F401
-    )
-    from pydantic.utils import (  # type: ignore[no-redef]
-        lenient_issubclass as lenient_issubclass,  # noqa: F401
-    )
-
-    GetJsonSchemaHandler = Any  # type: ignore[assignment,misc]
-    JsonSchemaValue = Dict[str, Any]  # type: ignore[misc]
-    CoreSchema = Any  # type: ignore[assignment,misc]
-
-    sequence_shapes = {
-        SHAPE_LIST,
-        SHAPE_SET,
-        SHAPE_FROZENSET,
-        SHAPE_TUPLE,
-        SHAPE_SEQUENCE,
-        SHAPE_TUPLE_ELLIPSIS,
-    }
-    sequence_shape_to_type = {
-        SHAPE_LIST: list,
-        SHAPE_SET: set,
-        SHAPE_TUPLE: tuple,
-        SHAPE_SEQUENCE: list,
-        SHAPE_TUPLE_ELLIPSIS: list,
-    }
-
-    @dataclass
-    class GenerateJsonSchema:  # type: ignore[no-redef]
-        ref_template: str
-
-    class PydanticSchemaGenerationError(Exception):  # type: ignore[no-redef]
-        pass
-
-    def with_info_plain_validator_function(  # type: ignore[misc]
-        function: Callable[..., Any],
-        *,
-        ref: Union[str, None] = None,
-        metadata: Any = None,
-        serialization: Any = None,
-    ) -> Any:
-        return {}
-
-    def get_model_definitions(
-        *,
-        flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
-        model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
-    ) -> Dict[str, Any]:
-        definitions: Dict[str, Dict[str, Any]] = {}
-        for model in flat_models:
-            m_schema, m_definitions, m_nested_models = model_process_schema(
-                model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
-            )
-            definitions.update(m_definitions)
-            model_name = model_name_map[model]
-            definitions[model_name] = m_schema
-        for m_schema in definitions.values():
-            if "description" in m_schema:
-                m_schema["description"] = m_schema["description"].split("\f")[0]
-        return definitions
-
-    def is_pv1_scalar_field(field: ModelField) -> bool:
-        from fastapi import params
-
-        field_info = field.field_info
-        if not (
-            field.shape == SHAPE_SINGLETON  # type: ignore[attr-defined]
-            and not lenient_issubclass(field.type_, BaseModel)
-            and not lenient_issubclass(field.type_, dict)
-            and not field_annotation_is_sequence(field.type_)
-            and not is_dataclass(field.type_)
-            and not isinstance(field_info, params.Body)
-        ):
-            return False
-        if field.sub_fields:  # type: ignore[attr-defined]
-            if not all(
-                is_pv1_scalar_field(f)
-                for f in field.sub_fields  # type: ignore[attr-defined]
-            ):
-                return False
-        return True
-
-    def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
-        if (field.shape in sequence_shapes) and not lenient_issubclass(  # type: ignore[attr-defined]
-            field.type_, BaseModel
-        ):
-            if field.sub_fields is not None:  # type: ignore[attr-defined]
-                for sub_field in field.sub_fields:  # type: ignore[attr-defined]
-                    if not is_pv1_scalar_field(sub_field):
-                        return False
-            return True
-        if _annotation_is_sequence(field.type_):
-            return True
-        return False
-
-    def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
-        use_errors: List[Any] = []
-        for error in errors:
-            if isinstance(error, ErrorWrapper):
-                new_errors = ValidationError(  # type: ignore[call-arg]
-                    errors=[error], model=RequestErrorModel
-                ).errors()
-                use_errors.extend(new_errors)
-            elif isinstance(error, list):
-                use_errors.extend(_normalize_errors(error))
-            else:
-                use_errors.append(error)
-        return use_errors
-
-    def _model_rebuild(model: Type[BaseModel]) -> None:
-        model.update_forward_refs()
-
-    def _model_dump(
-        model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
-    ) -> Any:
-        return model.dict(**kwargs)
-
-    def _get_model_config(model: BaseModel) -> Any:
-        return model.__config__  # type: ignore[attr-defined]
-
-    def get_schema_from_model_field(
-        *,
-        field: ModelField,
-        schema_generator: GenerateJsonSchema,
-        model_name_map: ModelNameMap,
-        field_mapping: Dict[
-            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
-        ],
-        separate_input_output_schemas: bool = True,
-    ) -> Dict[str, Any]:
-        # This expects that GenerateJsonSchema was already used to generate the definitions
-        return field_schema(  # type: ignore[no-any-return]
-            field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
-        )[0]
-
-    def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
-        models = get_flat_models_from_fields(fields, known_models=set())
-        return get_model_name_map(models)  # type: ignore[no-any-return]
-
-    def get_definitions(
-        *,
-        fields: List[ModelField],
-        schema_generator: GenerateJsonSchema,
-        model_name_map: ModelNameMap,
-        separate_input_output_schemas: bool = True,
-    ) -> Tuple[
-        Dict[
-            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
-        ],
-        Dict[str, Dict[str, Any]],
-    ]:
-        models = get_flat_models_from_fields(fields, known_models=set())
-        return {}, get_model_definitions(
-            flat_models=models, model_name_map=model_name_map
-        )
-
-    def is_scalar_field(field: ModelField) -> bool:
-        return is_pv1_scalar_field(field)
-
-    def is_sequence_field(field: ModelField) -> bool:
-        return field.shape in sequence_shapes or _annotation_is_sequence(field.type_)  # type: ignore[attr-defined]
-
-    def is_scalar_sequence_field(field: ModelField) -> bool:
-        return is_pv1_scalar_sequence_field(field)
-
-    def is_bytes_field(field: ModelField) -> bool:
-        return lenient_issubclass(field.type_, bytes)
-
-    def is_bytes_sequence_field(field: ModelField) -> bool:
-        return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes)  # type: ignore[attr-defined]
-
-    def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
-        return copy(field_info)
-
-    def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
-        return sequence_shape_to_type[field.shape](value)  # type: ignore[no-any-return,attr-defined]
-
-    def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
-        missing_field_error = ErrorWrapper(MissingError(), loc=loc)  # type: ignore[call-arg]
-        new_error = ValidationError([missing_field_error], RequestErrorModel)
-        return new_error.errors()[0]  # type: ignore[return-value]
-
-    def create_body_model(
-        *, fields: Sequence[ModelField], model_name: str
-    ) -> Type[BaseModel]:
-        BodyModel = create_model(model_name)
-        for f in fields:
-            BodyModel.__fields__[f.name] = f  # type: ignore[index]
-        return BodyModel
-
-    def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
-        return list(model.__fields__.values())  # type: ignore[attr-defined]
-
-
-def _regenerate_error_with_loc(
-    *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
-) -> List[Dict[str, Any]]:
-    updated_loc_errors: List[Any] = [
-        {**err, "loc": loc_prefix + err.get("loc", ())}
-        for err in _normalize_errors(errors)
-    ]
-
-    return updated_loc_errors
-
-
-def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
-    if lenient_issubclass(annotation, (str, bytes)):
-        return False
-    return lenient_issubclass(annotation, sequence_types)
-
-
-def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
-    origin = get_origin(annotation)
-    if origin is Union or origin is UnionType:
-        for arg in get_args(annotation):
-            if field_annotation_is_sequence(arg):
-                return True
-        return False
-    return _annotation_is_sequence(annotation) or _annotation_is_sequence(
-        get_origin(annotation)
-    )
-
-
-def value_is_sequence(value: Any) -> bool:
-    return isinstance(value, sequence_types) and not isinstance(value, (str, bytes))  # type: ignore[arg-type]
-
-
-def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
-    return (
-        lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
-        or _annotation_is_sequence(annotation)
-        or is_dataclass(annotation)
-    )
-
-
-def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
-    origin = get_origin(annotation)
-    if origin is Union or origin is UnionType:
-        return any(field_annotation_is_complex(arg) for arg in get_args(annotation))
-
-    if origin is Annotated:
-        return field_annotation_is_complex(get_args(annotation)[0])
-
-    return (
-        _annotation_is_complex(annotation)
-        or _annotation_is_complex(origin)
-        or hasattr(origin, "__pydantic_core_schema__")
-        or hasattr(origin, "__get_pydantic_core_schema__")
-    )
-
-
-def field_annotation_is_scalar(annotation: Any) -> bool:
-    # handle Ellipsis here to make tuple[int, ...] work nicely
-    return annotation is Ellipsis or not field_annotation_is_complex(annotation)
-
-
-def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:
-    origin = get_origin(annotation)
-    if origin is Union or origin is UnionType:
-        at_least_one_scalar_sequence = False
-        for arg in get_args(annotation):
-            if field_annotation_is_scalar_sequence(arg):
-                at_least_one_scalar_sequence = True
-                continue
-            elif not field_annotation_is_scalar(arg):
-                return False
-        return at_least_one_scalar_sequence
-    return field_annotation_is_sequence(annotation) and all(
-        field_annotation_is_scalar(sub_annotation)
-        for sub_annotation in get_args(annotation)
-    )
-
-
-def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
-    if lenient_issubclass(annotation, bytes):
-        return True
-    origin = get_origin(annotation)
-    if origin is Union or origin is UnionType:
-        for arg in get_args(annotation):
-            if lenient_issubclass(arg, bytes):
-                return True
-    return False
-
-
-def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool:
-    if lenient_issubclass(annotation, UploadFile):
-        return True
-    origin = get_origin(annotation)
-    if origin is Union or origin is UnionType:
-        for arg in get_args(annotation):
-            if lenient_issubclass(arg, UploadFile):
-                return True
-    return False
-
-
-def is_bytes_sequence_annotation(annotation: Any) -> bool:
-    origin = get_origin(annotation)
-    if origin is Union or origin is UnionType:
-        at_least_one = False
-        for arg in get_args(annotation):
-            if is_bytes_sequence_annotation(arg):
-                at_least_one = True
-                continue
-        return at_least_one
-    return field_annotation_is_sequence(annotation) and all(
-        is_bytes_or_nonable_bytes_annotation(sub_annotation)
-        for sub_annotation in get_args(annotation)
-    )
-
-
-def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
-    origin = get_origin(annotation)
-    if origin is Union or origin is UnionType:
-        at_least_one = False
-        for arg in get_args(annotation):
-            if is_uploadfile_sequence_annotation(arg):
-                at_least_one = True
-                continue
-        return at_least_one
-    return field_annotation_is_sequence(annotation) and all(
-        is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
-        for sub_annotation in get_args(annotation)
-    )
-
-
-@lru_cache
-def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
-    return get_model_fields(model)
diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py
new file mode 100644 (file)
index 0000000..b2ae5ad
--- /dev/null
@@ -0,0 +1,50 @@
+from .main import BaseConfig as BaseConfig
+from .main import PydanticSchemaGenerationError as PydanticSchemaGenerationError
+from .main import RequiredParam as RequiredParam
+from .main import Undefined as Undefined
+from .main import UndefinedType as UndefinedType
+from .main import Url as Url
+from .main import Validator as Validator
+from .main import _get_model_config as _get_model_config
+from .main import _is_error_wrapper as _is_error_wrapper
+from .main import _is_model_class as _is_model_class
+from .main import _is_model_field as _is_model_field
+from .main import _is_undefined as _is_undefined
+from .main import _model_dump as _model_dump
+from .main import _model_rebuild as _model_rebuild
+from .main import copy_field_info as copy_field_info
+from .main import create_body_model as create_body_model
+from .main import evaluate_forwardref as evaluate_forwardref
+from .main import get_annotation_from_field_info as get_annotation_from_field_info
+from .main import get_cached_model_fields as get_cached_model_fields
+from .main import get_compat_model_name_map as get_compat_model_name_map
+from .main import get_definitions as get_definitions
+from .main import get_missing_field_error as get_missing_field_error
+from .main import get_schema_from_model_field as get_schema_from_model_field
+from .main import is_bytes_field as is_bytes_field
+from .main import is_bytes_sequence_field as is_bytes_sequence_field
+from .main import is_scalar_field as is_scalar_field
+from .main import is_scalar_sequence_field as is_scalar_sequence_field
+from .main import is_sequence_field as is_sequence_field
+from .main import serialize_sequence_value as serialize_sequence_value
+from .main import (
+    with_info_plain_validator_function as with_info_plain_validator_function,
+)
+from .model_field import ModelField as ModelField
+from .shared import PYDANTIC_V2 as PYDANTIC_V2
+from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
+from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1
+from .shared import field_annotation_is_scalar as field_annotation_is_scalar
+from .shared import (
+    is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation,
+)
+from .shared import (
+    is_uploadfile_sequence_annotation as is_uploadfile_sequence_annotation,
+)
+from .shared import lenient_issubclass as lenient_issubclass
+from .shared import sequence_types as sequence_types
+from .shared import value_is_sequence as value_is_sequence
+from .v1 import CoreSchema as CoreSchema
+from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
+from .v1 import JsonSchemaValue as JsonSchemaValue
+from .v1 import _normalize_errors as _normalize_errors
diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py
new file mode 100644 (file)
index 0000000..3f758f0
--- /dev/null
@@ -0,0 +1,305 @@
+from functools import lru_cache
+from typing import (
+    Any,
+    Dict,
+    List,
+    Sequence,
+    Tuple,
+    Type,
+)
+
+from fastapi._compat import v1
+from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass
+from fastapi.types import ModelNameMap
+from pydantic import BaseModel
+from typing_extensions import Literal
+
+from .model_field import ModelField
+
+if PYDANTIC_V2:
+    from .v2 import BaseConfig as BaseConfig
+    from .v2 import FieldInfo as FieldInfo
+    from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError
+    from .v2 import RequiredParam as RequiredParam
+    from .v2 import Undefined as Undefined
+    from .v2 import UndefinedType as UndefinedType
+    from .v2 import Url as Url
+    from .v2 import Validator as Validator
+    from .v2 import evaluate_forwardref as evaluate_forwardref
+    from .v2 import get_missing_field_error as get_missing_field_error
+    from .v2 import (
+        with_info_plain_validator_function as with_info_plain_validator_function,
+    )
+else:
+    from .v1 import BaseConfig as BaseConfig  # type: ignore[assignment]
+    from .v1 import FieldInfo as FieldInfo
+    from .v1 import (  # type: ignore[assignment]
+        PydanticSchemaGenerationError as PydanticSchemaGenerationError,
+    )
+    from .v1 import RequiredParam as RequiredParam
+    from .v1 import Undefined as Undefined
+    from .v1 import UndefinedType as UndefinedType
+    from .v1 import Url as Url  # type: ignore[assignment]
+    from .v1 import Validator as Validator
+    from .v1 import evaluate_forwardref as evaluate_forwardref
+    from .v1 import get_missing_field_error as get_missing_field_error
+    from .v1 import (  # type: ignore[assignment]
+        with_info_plain_validator_function as with_info_plain_validator_function,
+    )
+
+
+@lru_cache
+def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
+    if lenient_issubclass(model, v1.BaseModel):
+        return v1.get_model_fields(model)
+    else:
+        from . import v2
+
+        return v2.get_model_fields(model)  # type: ignore[return-value]
+
+
+def _is_undefined(value: object) -> bool:
+    if isinstance(value, v1.UndefinedType):
+        return True
+    elif PYDANTIC_V2:
+        from . import v2
+
+        return isinstance(value, v2.UndefinedType)
+    return False
+
+
+def _get_model_config(model: BaseModel) -> Any:
+    if isinstance(model, v1.BaseModel):
+        return v1._get_model_config(model)
+    elif PYDANTIC_V2:
+        from . import v2
+
+        return v2._get_model_config(model)
+
+
+def _model_dump(
+    model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
+) -> Any:
+    if isinstance(model, v1.BaseModel):
+        return v1._model_dump(model, mode=mode, **kwargs)
+    elif PYDANTIC_V2:
+        from . import v2
+
+        return v2._model_dump(model, mode=mode, **kwargs)
+
+
+def _is_error_wrapper(exc: Exception) -> bool:
+    if isinstance(exc, v1.ErrorWrapper):
+        return True
+    elif PYDANTIC_V2:
+        from . import v2
+
+        return isinstance(exc, v2.ErrorWrapper)
+    return False
+
+
+def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
+    if isinstance(field_info, v1.FieldInfo):
+        return v1.copy_field_info(field_info=field_info, annotation=annotation)
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.copy_field_info(field_info=field_info, annotation=annotation)
+
+
+def create_body_model(
+    *, fields: Sequence[ModelField], model_name: str
+) -> Type[BaseModel]:
+    if fields and isinstance(fields[0], v1.ModelField):
+        return v1.create_body_model(fields=fields, model_name=model_name)
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.create_body_model(fields=fields, model_name=model_name)  # type: ignore[arg-type]
+
+
+def get_annotation_from_field_info(
+    annotation: Any, field_info: FieldInfo, field_name: str
+) -> Any:
+    if isinstance(field_info, v1.FieldInfo):
+        return v1.get_annotation_from_field_info(
+            annotation=annotation, field_info=field_info, field_name=field_name
+        )
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.get_annotation_from_field_info(
+            annotation=annotation, field_info=field_info, field_name=field_name
+        )
+
+
+def is_bytes_field(field: ModelField) -> bool:
+    if isinstance(field, v1.ModelField):
+        return v1.is_bytes_field(field)
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.is_bytes_field(field)  # type: ignore[arg-type]
+
+
+def is_bytes_sequence_field(field: ModelField) -> bool:
+    if isinstance(field, v1.ModelField):
+        return v1.is_bytes_sequence_field(field)
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.is_bytes_sequence_field(field)  # type: ignore[arg-type]
+
+
+def is_scalar_field(field: ModelField) -> bool:
+    if isinstance(field, v1.ModelField):
+        return v1.is_scalar_field(field)
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.is_scalar_field(field)  # type: ignore[arg-type]
+
+
+def is_scalar_sequence_field(field: ModelField) -> bool:
+    if isinstance(field, v1.ModelField):
+        return v1.is_scalar_sequence_field(field)
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.is_scalar_sequence_field(field)  # type: ignore[arg-type]
+
+
+def is_sequence_field(field: ModelField) -> bool:
+    if isinstance(field, v1.ModelField):
+        return v1.is_sequence_field(field)
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.is_sequence_field(field)  # type: ignore[arg-type]
+
+
+def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
+    if isinstance(field, v1.ModelField):
+        return v1.serialize_sequence_value(field=field, value=value)
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.serialize_sequence_value(field=field, value=value)  # type: ignore[arg-type]
+
+
+def _model_rebuild(model: Type[BaseModel]) -> None:
+    if lenient_issubclass(model, v1.BaseModel):
+        v1._model_rebuild(model)
+    elif PYDANTIC_V2:
+        from . import v2
+
+        v2._model_rebuild(model)
+
+
+def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
+    v1_model_fields = [field for field in fields if isinstance(field, v1.ModelField)]
+    v1_flat_models = v1.get_flat_models_from_fields(v1_model_fields, known_models=set())  # type: ignore[attr-defined]
+    all_flat_models = v1_flat_models
+    if PYDANTIC_V2:
+        from . import v2
+
+        v2_model_fields = [
+            field for field in fields if isinstance(field, v2.ModelField)
+        ]
+        v2_flat_models = v2.get_flat_models_from_fields(
+            v2_model_fields, known_models=set()
+        )
+        all_flat_models = all_flat_models.union(v2_flat_models)
+
+        model_name_map = v2.get_model_name_map(all_flat_models)
+        return model_name_map
+    model_name_map = v1.get_model_name_map(all_flat_models)
+    return model_name_map
+
+
+def get_definitions(
+    *,
+    fields: List[ModelField],
+    model_name_map: ModelNameMap,
+    separate_input_output_schemas: bool = True,
+) -> Tuple[
+    Dict[Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue],
+    Dict[str, Dict[str, Any]],
+]:
+    v1_fields = [field for field in fields if isinstance(field, v1.ModelField)]
+    v1_field_maps, v1_definitions = v1.get_definitions(
+        fields=v1_fields,
+        model_name_map=model_name_map,
+        separate_input_output_schemas=separate_input_output_schemas,
+    )
+    if not PYDANTIC_V2:
+        return v1_field_maps, v1_definitions
+    else:
+        from . import v2
+
+        v2_fields = [field for field in fields if isinstance(field, v2.ModelField)]
+        v2_field_maps, v2_definitions = v2.get_definitions(
+            fields=v2_fields,
+            model_name_map=model_name_map,
+            separate_input_output_schemas=separate_input_output_schemas,
+        )
+        all_definitions = {**v1_definitions, **v2_definitions}
+        all_field_maps = {**v1_field_maps, **v2_field_maps}
+        return all_field_maps, all_definitions
+
+
+def get_schema_from_model_field(
+    *,
+    field: ModelField,
+    model_name_map: ModelNameMap,
+    field_mapping: Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue
+    ],
+    separate_input_output_schemas: bool = True,
+) -> Dict[str, Any]:
+    if isinstance(field, v1.ModelField):
+        return v1.get_schema_from_model_field(
+            field=field,
+            model_name_map=model_name_map,
+            field_mapping=field_mapping,
+            separate_input_output_schemas=separate_input_output_schemas,
+        )
+    else:
+        assert PYDANTIC_V2
+        from . import v2
+
+        return v2.get_schema_from_model_field(
+            field=field,  # type: ignore[arg-type]
+            model_name_map=model_name_map,
+            field_mapping=field_mapping,  # type: ignore[arg-type]
+            separate_input_output_schemas=separate_input_output_schemas,
+        )
+
+
+def _is_model_field(value: Any) -> bool:
+    if isinstance(value, v1.ModelField):
+        return True
+    elif PYDANTIC_V2:
+        from . import v2
+
+        return isinstance(value, v2.ModelField)
+    return False
+
+
+def _is_model_class(value: Any) -> bool:
+    if lenient_issubclass(value, v1.BaseModel):
+        return True
+    elif PYDANTIC_V2:
+        from . import v2
+
+        return lenient_issubclass(value, v2.BaseModel)  # type: ignore[attr-defined]
+    return False
diff --git a/fastapi/_compat/model_field.py b/fastapi/_compat/model_field.py
new file mode 100644 (file)
index 0000000..fa2008c
--- /dev/null
@@ -0,0 +1,53 @@
+from typing import (
+    Any,
+    Dict,
+    List,
+    Tuple,
+    Union,
+)
+
+from fastapi.types import IncEx
+from pydantic.fields import FieldInfo
+from typing_extensions import Literal, Protocol
+
+
+class ModelField(Protocol):
+    field_info: "FieldInfo"
+    name: str
+    mode: Literal["validation", "serialization"] = "validation"
+    _version: Literal["v1", "v2"] = "v1"
+
+    @property
+    def alias(self) -> str: ...
+
+    @property
+    def required(self) -> bool: ...
+
+    @property
+    def default(self) -> Any: ...
+
+    @property
+    def type_(self) -> Any: ...
+
+    def get_default(self) -> Any: ...
+
+    def validate(
+        self,
+        value: Any,
+        values: Dict[str, Any] = {},  # noqa: B006
+        *,
+        loc: Tuple[Union[int, str], ...] = (),
+    ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: ...
+
+    def serialize(
+        self,
+        value: Any,
+        *,
+        mode: Literal["json", "python"] = "json",
+        include: Union[IncEx, None] = None,
+        exclude: Union[IncEx, None] = None,
+        by_alias: bool = True,
+        exclude_unset: bool = False,
+        exclude_defaults: bool = False,
+        exclude_none: bool = False,
+    ) -> Any: ...
diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py
new file mode 100644 (file)
index 0000000..495d5c5
--- /dev/null
@@ -0,0 +1,209 @@
+import sys
+import types
+import typing
+from collections import deque
+from dataclasses import is_dataclass
+from typing import (
+    Any,
+    Deque,
+    FrozenSet,
+    List,
+    Mapping,
+    Sequence,
+    Set,
+    Tuple,
+    Type,
+    Union,
+)
+
+from fastapi._compat import v1
+from fastapi.types import UnionType
+from pydantic import BaseModel
+from pydantic.version import VERSION as PYDANTIC_VERSION
+from starlette.datastructures import UploadFile
+from typing_extensions import Annotated, get_args, get_origin
+
+# Copy from Pydantic v2, compatible with v1
+if sys.version_info < (3, 9):
+    # Pydantic no longer supports Python 3.8, this might be incorrect, but the code
+    # this is used for is also never reached in this codebase, as it's a copy of
+    # Pydantic's lenient_issubclass, just for compatibility with v1
+    # TODO: remove when dropping support for Python 3.8
+    WithArgsTypes: Tuple[Any, ...] = ()
+elif sys.version_info < (3, 10):
+    WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias)  # type: ignore[attr-defined]
+else:
+    WithArgsTypes: tuple[Any, ...] = (
+        typing._GenericAlias,  # type: ignore[attr-defined]
+        types.GenericAlias,
+        types.UnionType,
+    )  # pyright: ignore[reportAttributeAccessIssue]
+
+PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2])
+PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
+
+
+sequence_annotation_to_type = {
+    Sequence: list,
+    List: list,
+    list: list,
+    Tuple: tuple,
+    tuple: tuple,
+    Set: set,
+    set: set,
+    FrozenSet: frozenset,
+    frozenset: frozenset,
+    Deque: deque,
+    deque: deque,
+}
+
+sequence_types = tuple(sequence_annotation_to_type.keys())
+
+Url: Type[Any]
+
+
+# Copy of Pydantic v2, compatible with v1
+def lenient_issubclass(
+    cls: Any, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...], None]
+) -> bool:
+    try:
+        return isinstance(cls, type) and issubclass(cls, class_or_tuple)  # type: ignore[arg-type]
+    except TypeError:  # pragma: no cover
+        if isinstance(cls, WithArgsTypes):
+            return False
+        raise  # pragma: no cover
+
+
+def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
+    if lenient_issubclass(annotation, (str, bytes)):
+        return False
+    return lenient_issubclass(annotation, sequence_types)  # type: ignore[arg-type]
+
+
+def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        for arg in get_args(annotation):
+            if field_annotation_is_sequence(arg):
+                return True
+        return False
+    return _annotation_is_sequence(annotation) or _annotation_is_sequence(
+        get_origin(annotation)
+    )
+
+
+def value_is_sequence(value: Any) -> bool:
+    return isinstance(value, sequence_types) and not isinstance(value, (str, bytes))  # type: ignore[arg-type]
+
+
+def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
+    return (
+        lenient_issubclass(annotation, (BaseModel, v1.BaseModel, Mapping, UploadFile))
+        or _annotation_is_sequence(annotation)
+        or is_dataclass(annotation)
+    )
+
+
+def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        return any(field_annotation_is_complex(arg) for arg in get_args(annotation))
+
+    if origin is Annotated:
+        return field_annotation_is_complex(get_args(annotation)[0])
+
+    return (
+        _annotation_is_complex(annotation)
+        or _annotation_is_complex(origin)
+        or hasattr(origin, "__pydantic_core_schema__")
+        or hasattr(origin, "__get_pydantic_core_schema__")
+    )
+
+
+def field_annotation_is_scalar(annotation: Any) -> bool:
+    # handle Ellipsis here to make tuple[int, ...] work nicely
+    return annotation is Ellipsis or not field_annotation_is_complex(annotation)
+
+
+def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        at_least_one_scalar_sequence = False
+        for arg in get_args(annotation):
+            if field_annotation_is_scalar_sequence(arg):
+                at_least_one_scalar_sequence = True
+                continue
+            elif not field_annotation_is_scalar(arg):
+                return False
+        return at_least_one_scalar_sequence
+    return field_annotation_is_sequence(annotation) and all(
+        field_annotation_is_scalar(sub_annotation)
+        for sub_annotation in get_args(annotation)
+    )
+
+
+def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
+    if lenient_issubclass(annotation, bytes):
+        return True
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        for arg in get_args(annotation):
+            if lenient_issubclass(arg, bytes):
+                return True
+    return False
+
+
+def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool:
+    if lenient_issubclass(annotation, UploadFile):
+        return True
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        for arg in get_args(annotation):
+            if lenient_issubclass(arg, UploadFile):
+                return True
+    return False
+
+
+def is_bytes_sequence_annotation(annotation: Any) -> bool:
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        at_least_one = False
+        for arg in get_args(annotation):
+            if is_bytes_sequence_annotation(arg):
+                at_least_one = True
+                continue
+        return at_least_one
+    return field_annotation_is_sequence(annotation) and all(
+        is_bytes_or_nonable_bytes_annotation(sub_annotation)
+        for sub_annotation in get_args(annotation)
+    )
+
+
+def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        at_least_one = False
+        for arg in get_args(annotation):
+            if is_uploadfile_sequence_annotation(arg):
+                at_least_one = True
+                continue
+        return at_least_one
+    return field_annotation_is_sequence(annotation) and all(
+        is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
+        for sub_annotation in get_args(annotation)
+    )
+
+
+def annotation_is_pydantic_v1(annotation: Any) -> bool:
+    if lenient_issubclass(annotation, v1.BaseModel):
+        return True
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        for arg in get_args(annotation):
+            if lenient_issubclass(arg, v1.BaseModel):
+                return True
+    if field_annotation_is_sequence(annotation):
+        for sub_annotation in get_args(annotation):
+            if annotation_is_pydantic_v1(sub_annotation):
+                return True
+    return False
diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py
new file mode 100644 (file)
index 0000000..f0ac516
--- /dev/null
@@ -0,0 +1,334 @@
+from copy import copy
+from dataclasses import dataclass, is_dataclass
+from enum import Enum
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    List,
+    Sequence,
+    Set,
+    Tuple,
+    Type,
+    Union,
+)
+
+from fastapi._compat import shared
+from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
+from fastapi.types import ModelNameMap
+from pydantic.version import VERSION as PYDANTIC_VERSION
+from typing_extensions import Literal
+
+PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2])
+PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
+# Keeping old "Required" functionality from Pydantic V1, without
+# shadowing typing.Required.
+RequiredParam: Any = Ellipsis
+
+if not PYDANTIC_V2:
+    from pydantic import BaseConfig as BaseConfig
+    from pydantic import BaseModel as BaseModel
+    from pydantic import ValidationError as ValidationError
+    from pydantic import create_model as create_model
+    from pydantic.class_validators import Validator as Validator
+    from pydantic.color import Color as Color
+    from pydantic.error_wrappers import ErrorWrapper as ErrorWrapper
+    from pydantic.errors import MissingError
+    from pydantic.fields import (  # type: ignore[attr-defined]
+        SHAPE_FROZENSET,
+        SHAPE_LIST,
+        SHAPE_SEQUENCE,
+        SHAPE_SET,
+        SHAPE_SINGLETON,
+        SHAPE_TUPLE,
+        SHAPE_TUPLE_ELLIPSIS,
+    )
+    from pydantic.fields import FieldInfo as FieldInfo
+    from pydantic.fields import ModelField as ModelField  # type: ignore[attr-defined]
+    from pydantic.fields import Undefined as Undefined  # type: ignore[attr-defined]
+    from pydantic.fields import (  # type: ignore[attr-defined]
+        UndefinedType as UndefinedType,
+    )
+    from pydantic.networks import AnyUrl as AnyUrl
+    from pydantic.networks import NameEmail as NameEmail
+    from pydantic.schema import TypeModelSet as TypeModelSet
+    from pydantic.schema import (
+        field_schema,
+        get_flat_models_from_fields,
+        model_process_schema,
+    )
+    from pydantic.schema import (
+        get_annotation_from_field_info as get_annotation_from_field_info,
+    )
+    from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field
+    from pydantic.schema import get_model_name_map as get_model_name_map
+    from pydantic.types import SecretBytes as SecretBytes
+    from pydantic.types import SecretStr as SecretStr
+    from pydantic.typing import evaluate_forwardref as evaluate_forwardref
+    from pydantic.utils import lenient_issubclass as lenient_issubclass
+
+
+else:
+    from pydantic.v1 import BaseConfig as BaseConfig  # type: ignore[assignment]
+    from pydantic.v1 import BaseModel as BaseModel  # type: ignore[assignment]
+    from pydantic.v1 import (  # type: ignore[assignment]
+        ValidationError as ValidationError,
+    )
+    from pydantic.v1 import create_model as create_model  # type: ignore[no-redef]
+    from pydantic.v1.class_validators import Validator as Validator
+    from pydantic.v1.color import Color as Color  # type: ignore[assignment]
+    from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper
+    from pydantic.v1.errors import MissingError
+    from pydantic.v1.fields import (
+        SHAPE_FROZENSET,
+        SHAPE_LIST,
+        SHAPE_SEQUENCE,
+        SHAPE_SET,
+        SHAPE_SINGLETON,
+        SHAPE_TUPLE,
+        SHAPE_TUPLE_ELLIPSIS,
+    )
+    from pydantic.v1.fields import FieldInfo as FieldInfo  # type: ignore[assignment]
+    from pydantic.v1.fields import ModelField as ModelField
+    from pydantic.v1.fields import Undefined as Undefined
+    from pydantic.v1.fields import UndefinedType as UndefinedType
+    from pydantic.v1.networks import AnyUrl as AnyUrl
+    from pydantic.v1.networks import (  # type: ignore[assignment]
+        NameEmail as NameEmail,
+    )
+    from pydantic.v1.schema import TypeModelSet as TypeModelSet
+    from pydantic.v1.schema import (
+        field_schema,
+        get_flat_models_from_fields,
+        model_process_schema,
+    )
+    from pydantic.v1.schema import (
+        get_annotation_from_field_info as get_annotation_from_field_info,
+    )
+    from pydantic.v1.schema import (
+        get_flat_models_from_field as get_flat_models_from_field,
+    )
+    from pydantic.v1.schema import get_model_name_map as get_model_name_map
+    from pydantic.v1.types import (  # type: ignore[assignment]
+        SecretBytes as SecretBytes,
+    )
+    from pydantic.v1.types import (  # type: ignore[assignment]
+        SecretStr as SecretStr,
+    )
+    from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref
+    from pydantic.v1.utils import lenient_issubclass as lenient_issubclass
+
+
+GetJsonSchemaHandler = Any
+JsonSchemaValue = Dict[str, Any]
+CoreSchema = Any
+Url = AnyUrl
+
+sequence_shapes = {
+    SHAPE_LIST,
+    SHAPE_SET,
+    SHAPE_FROZENSET,
+    SHAPE_TUPLE,
+    SHAPE_SEQUENCE,
+    SHAPE_TUPLE_ELLIPSIS,
+}
+sequence_shape_to_type = {
+    SHAPE_LIST: list,
+    SHAPE_SET: set,
+    SHAPE_TUPLE: tuple,
+    SHAPE_SEQUENCE: list,
+    SHAPE_TUPLE_ELLIPSIS: list,
+}
+
+
+@dataclass
+class GenerateJsonSchema:
+    ref_template: str
+
+
+class PydanticSchemaGenerationError(Exception):
+    pass
+
+
+RequestErrorModel: Type[BaseModel] = create_model("Request")
+
+
+def with_info_plain_validator_function(
+    function: Callable[..., Any],
+    *,
+    ref: Union[str, None] = None,
+    metadata: Any = None,
+    serialization: Any = None,
+) -> Any:
+    return {}
+
+
+def get_model_definitions(
+    *,
+    flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
+    model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
+) -> Dict[str, Any]:
+    definitions: Dict[str, Dict[str, Any]] = {}
+    for model in flat_models:
+        m_schema, m_definitions, m_nested_models = model_process_schema(
+            model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
+        )
+        definitions.update(m_definitions)
+        model_name = model_name_map[model]
+        definitions[model_name] = m_schema
+    for m_schema in definitions.values():
+        if "description" in m_schema:
+            m_schema["description"] = m_schema["description"].split("\f")[0]
+    return definitions
+
+
+def is_pv1_scalar_field(field: ModelField) -> bool:
+    from fastapi import params
+
+    field_info = field.field_info
+    if not (
+        field.shape == SHAPE_SINGLETON
+        and not lenient_issubclass(field.type_, BaseModel)
+        and not lenient_issubclass(field.type_, dict)
+        and not shared.field_annotation_is_sequence(field.type_)
+        and not is_dataclass(field.type_)
+        and not isinstance(field_info, params.Body)
+    ):
+        return False
+    if field.sub_fields:
+        if not all(is_pv1_scalar_field(f) for f in field.sub_fields):
+            return False
+    return True
+
+
+def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
+    if (field.shape in sequence_shapes) and not lenient_issubclass(
+        field.type_, BaseModel
+    ):
+        if field.sub_fields is not None:
+            for sub_field in field.sub_fields:
+                if not is_pv1_scalar_field(sub_field):
+                    return False
+        return True
+    if shared._annotation_is_sequence(field.type_):
+        return True
+    return False
+
+
+def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
+    use_errors: List[Any] = []
+    for error in errors:
+        if isinstance(error, ErrorWrapper):
+            new_errors = ValidationError(  # type: ignore[call-arg]
+                errors=[error], model=RequestErrorModel
+            ).errors()
+            use_errors.extend(new_errors)
+        elif isinstance(error, list):
+            use_errors.extend(_normalize_errors(error))
+        else:
+            use_errors.append(error)
+    return use_errors
+
+
+def _regenerate_error_with_loc(
+    *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
+) -> List[Dict[str, Any]]:
+    updated_loc_errors: List[Any] = [
+        {**err, "loc": loc_prefix + err.get("loc", ())}
+        for err in _normalize_errors(errors)
+    ]
+
+    return updated_loc_errors
+
+
+def _model_rebuild(model: Type[BaseModel]) -> None:
+    model.update_forward_refs()
+
+
+def _model_dump(
+    model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
+) -> Any:
+    return model.dict(**kwargs)
+
+
+def _get_model_config(model: BaseModel) -> Any:
+    return model.__config__  # type: ignore[attr-defined]
+
+
+def get_schema_from_model_field(
+    *,
+    field: ModelField,
+    model_name_map: ModelNameMap,
+    field_mapping: Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+    ],
+    separate_input_output_schemas: bool = True,
+) -> Dict[str, Any]:
+    return field_schema(  # type: ignore[no-any-return]
+        field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
+    )[0]
+
+
+# def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
+#     models = get_flat_models_from_fields(fields, known_models=set())
+#     return get_model_name_map(models)  # type: ignore[no-any-return]
+
+
+def get_definitions(
+    *,
+    fields: List[ModelField],
+    model_name_map: ModelNameMap,
+    separate_input_output_schemas: bool = True,
+) -> Tuple[
+    Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
+    Dict[str, Dict[str, Any]],
+]:
+    models = get_flat_models_from_fields(fields, known_models=set())
+    return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map)
+
+
+def is_scalar_field(field: ModelField) -> bool:
+    return is_pv1_scalar_field(field)
+
+
+def is_sequence_field(field: ModelField) -> bool:
+    return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_)
+
+
+def is_scalar_sequence_field(field: ModelField) -> bool:
+    return is_pv1_scalar_sequence_field(field)
+
+
+def is_bytes_field(field: ModelField) -> bool:
+    return lenient_issubclass(field.type_, bytes)  # type: ignore[no-any-return]
+
+
+def is_bytes_sequence_field(field: ModelField) -> bool:
+    return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes)
+
+
+def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
+    return copy(field_info)
+
+
+def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
+    return sequence_shape_to_type[field.shape](value)  # type: ignore[no-any-return]
+
+
+def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
+    missing_field_error = ErrorWrapper(MissingError(), loc=loc)
+    new_error = ValidationError([missing_field_error], RequestErrorModel)
+    return new_error.errors()[0]  # type: ignore[return-value]
+
+
+def create_body_model(
+    *, fields: Sequence[ModelField], model_name: str
+) -> Type[BaseModel]:
+    BodyModel = create_model(model_name)
+    for f in fields:
+        BodyModel.__fields__[f.name] = f  # type: ignore[index]
+    return BodyModel
+
+
+def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
+    return list(model.__fields__.values())  # type: ignore[attr-defined]
diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py
new file mode 100644 (file)
index 0000000..29606b9
--- /dev/null
@@ -0,0 +1,459 @@
+import re
+import warnings
+from copy import copy, deepcopy
+from dataclasses import dataclass
+from enum import Enum
+from typing import (
+    Any,
+    Dict,
+    List,
+    Sequence,
+    Set,
+    Tuple,
+    Type,
+    Union,
+    cast,
+)
+
+from fastapi._compat import shared, v1
+from fastapi.openapi.constants import REF_TEMPLATE
+from fastapi.types import IncEx, ModelNameMap
+from pydantic import BaseModel, TypeAdapter, create_model
+from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
+from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
+from pydantic import ValidationError as ValidationError
+from pydantic._internal._schema_generation_shared import (  # type: ignore[attr-defined]
+    GetJsonSchemaHandler as GetJsonSchemaHandler,
+)
+from pydantic._internal._typing_extra import eval_type_lenient
+from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
+from pydantic.fields import FieldInfo as FieldInfo
+from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
+from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
+from pydantic_core import CoreSchema as CoreSchema
+from pydantic_core import PydanticUndefined, PydanticUndefinedType
+from pydantic_core import Url as Url
+from typing_extensions import Annotated, Literal, get_args, get_origin
+
+try:
+    from pydantic_core.core_schema import (
+        with_info_plain_validator_function as with_info_plain_validator_function,
+    )
+except ImportError:  # pragma: no cover
+    from pydantic_core.core_schema import (
+        general_plain_validator_function as with_info_plain_validator_function,  # noqa: F401
+    )
+
+RequiredParam = PydanticUndefined
+Undefined = PydanticUndefined
+UndefinedType = PydanticUndefinedType
+evaluate_forwardref = eval_type_lenient
+Validator = Any
+
+
+class BaseConfig:
+    pass
+
+
+class ErrorWrapper(Exception):
+    pass
+
+
+@dataclass
+class ModelField:
+    field_info: FieldInfo
+    name: str
+    mode: Literal["validation", "serialization"] = "validation"
+
+    @property
+    def alias(self) -> str:
+        a = self.field_info.alias
+        return a if a is not None else self.name
+
+    @property
+    def required(self) -> bool:
+        return self.field_info.is_required()
+
+    @property
+    def default(self) -> Any:
+        return self.get_default()
+
+    @property
+    def type_(self) -> Any:
+        return self.field_info.annotation
+
+    def __post_init__(self) -> None:
+        with warnings.catch_warnings():
+            # Pydantic >= 2.12.0 warns about field specific metadata that is unused
+            # (e.g. `TypeAdapter(Annotated[int, Field(alias='b')])`). In some cases, we
+            # end up building the type adapter from a model field annotation so we
+            # need to ignore the warning:
+            if shared.PYDANTIC_VERSION_MINOR_TUPLE >= (2, 12):
+                from pydantic.warnings import UnsupportedFieldAttributeWarning
+
+                warnings.simplefilter(
+                    "ignore", category=UnsupportedFieldAttributeWarning
+                )
+            self._type_adapter: TypeAdapter[Any] = TypeAdapter(
+                Annotated[self.field_info.annotation, self.field_info]
+            )
+
+    def get_default(self) -> Any:
+        if self.field_info.is_required():
+            return Undefined
+        return self.field_info.get_default(call_default_factory=True)
+
+    def validate(
+        self,
+        value: Any,
+        values: Dict[str, Any] = {},  # noqa: B006
+        *,
+        loc: Tuple[Union[int, str], ...] = (),
+    ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
+        try:
+            return (
+                self._type_adapter.validate_python(value, from_attributes=True),
+                None,
+            )
+        except ValidationError as exc:
+            return None, v1._regenerate_error_with_loc(
+                errors=exc.errors(include_url=False), loc_prefix=loc
+            )
+
+    def serialize(
+        self,
+        value: Any,
+        *,
+        mode: Literal["json", "python"] = "json",
+        include: Union[IncEx, None] = None,
+        exclude: Union[IncEx, None] = None,
+        by_alias: bool = True,
+        exclude_unset: bool = False,
+        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,
+        )
+
+    def __hash__(self) -> int:
+        # Each ModelField is unique for our purposes, to allow making a dict from
+        # ModelField to its JSON Schema.
+        return id(self)
+
+
+def get_annotation_from_field_info(
+    annotation: Any, field_info: FieldInfo, field_name: str
+) -> Any:
+    return annotation
+
+
+def _model_rebuild(model: Type[BaseModel]) -> None:
+    model.model_rebuild()
+
+
+def _model_dump(
+    model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
+) -> Any:
+    return model.model_dump(mode=mode, **kwargs)
+
+
+def _get_model_config(model: BaseModel) -> Any:
+    return model.model_config
+
+
+def get_schema_from_model_field(
+    *,
+    field: ModelField,
+    model_name_map: ModelNameMap,
+    field_mapping: Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+    ],
+    separate_input_output_schemas: bool = True,
+) -> Dict[str, Any]:
+    override_mode: Union[Literal["validation"], None] = (
+        None if separate_input_output_schemas else "validation"
+    )
+    # This expects that GenerateJsonSchema was already used to generate the definitions
+    json_schema = field_mapping[(field, override_mode or field.mode)]
+    if "$ref" not in json_schema:
+        # TODO remove when deprecating Pydantic v1
+        # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
+        json_schema["title"] = field.field_info.title or field.alias.title().replace(
+            "_", " "
+        )
+    return json_schema
+
+
+def get_definitions(
+    *,
+    fields: Sequence[ModelField],
+    model_name_map: ModelNameMap,
+    separate_input_output_schemas: bool = True,
+) -> Tuple[
+    Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
+    Dict[str, Dict[str, Any]],
+]:
+    schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
+    override_mode: Union[Literal["validation"], None] = (
+        None if separate_input_output_schemas else "validation"
+    )
+    flat_models = get_flat_models_from_fields(fields, known_models=set())
+    flat_model_fields = [
+        ModelField(field_info=FieldInfo(annotation=model), name=model.__name__)
+        for model in flat_models
+    ]
+    input_types = {f.type_ for f in fields}
+    unique_flat_model_fields = {
+        f for f in flat_model_fields if f.type_ not in input_types
+    }
+
+    inputs = [
+        (field, override_mode or field.mode, field._type_adapter.core_schema)
+        for field in list(fields) + list(unique_flat_model_fields)
+    ]
+    field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs)
+    for item_def in cast(Dict[str, Dict[str, Any]], definitions).values():
+        if "description" in item_def:
+            item_description = cast(str, item_def["description"]).split("\f")[0]
+            item_def["description"] = item_description
+    new_mapping, new_definitions = _remap_definitions_and_field_mappings(
+        model_name_map=model_name_map,
+        definitions=definitions,  # type: ignore[arg-type]
+        field_mapping=field_mapping,
+    )
+    return new_mapping, new_definitions
+
+
+def _replace_refs(
+    *,
+    schema: Dict[str, Any],
+    old_name_to_new_name_map: Dict[str, str],
+) -> Dict[str, Any]:
+    new_schema = deepcopy(schema)
+    for key, value in new_schema.items():
+        if key == "$ref":
+            ref_name = schema["$ref"].split("/")[-1]
+            if ref_name in old_name_to_new_name_map:
+                new_name = old_name_to_new_name_map[ref_name]
+                new_schema["$ref"] = REF_TEMPLATE.format(model=new_name)
+            else:
+                new_schema["$ref"] = schema["$ref"]
+            continue
+        if isinstance(value, dict):
+            new_schema[key] = _replace_refs(
+                schema=value,
+                old_name_to_new_name_map=old_name_to_new_name_map,
+            )
+        elif isinstance(value, list):
+            new_value = []
+            for item in value:
+                if isinstance(item, dict):
+                    new_item = _replace_refs(
+                        schema=item,
+                        old_name_to_new_name_map=old_name_to_new_name_map,
+                    )
+                    new_value.append(new_item)
+
+                else:
+                    new_value.append(item)
+            new_schema[key] = new_value
+    return new_schema
+
+
+def _remap_definitions_and_field_mappings(
+    *,
+    model_name_map: ModelNameMap,
+    definitions: Dict[str, Any],
+    field_mapping: Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+    ],
+) -> Tuple[
+    Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
+    Dict[str, Any],
+]:
+    old_name_to_new_name_map = {}
+    for field_key, schema in field_mapping.items():
+        model = field_key[0].type_
+        if model not in model_name_map:
+            continue
+        new_name = model_name_map[model]
+        old_name = schema["$ref"].split("/")[-1]
+        if old_name in {f"{new_name}-Input", f"{new_name}-Output"}:
+            continue
+        old_name_to_new_name_map[old_name] = new_name
+
+    new_field_mapping: Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+    ] = {}
+    for field_key, schema in field_mapping.items():
+        new_schema = _replace_refs(
+            schema=schema,
+            old_name_to_new_name_map=old_name_to_new_name_map,
+        )
+        new_field_mapping[field_key] = new_schema
+
+    new_definitions = {}
+    for key, value in definitions.items():
+        if key in old_name_to_new_name_map:
+            new_key = old_name_to_new_name_map[key]
+        else:
+            new_key = key
+        new_value = _replace_refs(
+            schema=value,
+            old_name_to_new_name_map=old_name_to_new_name_map,
+        )
+        new_definitions[new_key] = new_value
+    return new_field_mapping, new_definitions
+
+
+def is_scalar_field(field: ModelField) -> bool:
+    from fastapi import params
+
+    return shared.field_annotation_is_scalar(
+        field.field_info.annotation
+    ) and not isinstance(field.field_info, params.Body)
+
+
+def is_sequence_field(field: ModelField) -> bool:
+    return shared.field_annotation_is_sequence(field.field_info.annotation)
+
+
+def is_scalar_sequence_field(field: ModelField) -> bool:
+    return shared.field_annotation_is_scalar_sequence(field.field_info.annotation)
+
+
+def is_bytes_field(field: ModelField) -> bool:
+    return shared.is_bytes_or_nonable_bytes_annotation(field.type_)
+
+
+def is_bytes_sequence_field(field: ModelField) -> bool:
+    return shared.is_bytes_sequence_annotation(field.type_)
+
+
+def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
+    cls = type(field_info)
+    merged_field_info = cls.from_annotation(annotation)
+    new_field_info = copy(field_info)
+    new_field_info.metadata = merged_field_info.metadata
+    new_field_info.annotation = merged_field_info.annotation
+    return new_field_info
+
+
+def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
+    origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation
+    assert issubclass(origin_type, shared.sequence_types)  # type: ignore[arg-type]
+    return shared.sequence_annotation_to_type[origin_type](value)  # type: ignore[no-any-return]
+
+
+def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
+    error = ValidationError.from_exception_data(
+        "Field required", [{"type": "missing", "loc": loc, "input": {}}]
+    ).errors(include_url=False)[0]
+    error["input"] = None
+    return error  # type: ignore[return-value]
+
+
+def create_body_model(
+    *, fields: Sequence[ModelField], model_name: str
+) -> Type[BaseModel]:
+    field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
+    BodyModel: Type[BaseModel] = create_model(model_name, **field_params)  # type: ignore[call-overload]
+    return BodyModel
+
+
+def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
+    return [
+        ModelField(field_info=field_info, name=name)
+        for name, field_info in model.model_fields.items()
+    ]
+
+
+# Duplicate of several schema functions from Pydantic v1 to make them compatible with
+# Pydantic v2 and allow mixing the models
+
+TypeModelOrEnum = Union[Type["BaseModel"], Type[Enum]]
+TypeModelSet = Set[TypeModelOrEnum]
+
+
+def normalize_name(name: str) -> str:
+    return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name)
+
+
+def get_model_name_map(unique_models: TypeModelSet) -> Dict[TypeModelOrEnum, str]:
+    name_model_map = {}
+    conflicting_names: Set[str] = set()
+    for model in unique_models:
+        model_name = normalize_name(model.__name__)
+        if model_name in conflicting_names:
+            model_name = get_long_model_name(model)
+            name_model_map[model_name] = model
+        elif model_name in name_model_map:
+            conflicting_names.add(model_name)
+            conflicting_model = name_model_map.pop(model_name)
+            name_model_map[get_long_model_name(conflicting_model)] = conflicting_model
+            name_model_map[get_long_model_name(model)] = model
+        else:
+            name_model_map[model_name] = model
+    return {v: k for k, v in name_model_map.items()}
+
+
+def get_flat_models_from_model(
+    model: Type["BaseModel"], known_models: Union[TypeModelSet, None] = None
+) -> TypeModelSet:
+    known_models = known_models or set()
+    fields = get_model_fields(model)
+    get_flat_models_from_fields(fields, known_models=known_models)
+    return known_models
+
+
+def get_flat_models_from_annotation(
+    annotation: Any, known_models: TypeModelSet
+) -> TypeModelSet:
+    origin = get_origin(annotation)
+    if origin is not None:
+        for arg in get_args(annotation):
+            if lenient_issubclass(arg, (BaseModel, Enum)) and arg not in known_models:
+                known_models.add(arg)
+                if lenient_issubclass(arg, BaseModel):
+                    get_flat_models_from_model(arg, known_models=known_models)
+            else:
+                get_flat_models_from_annotation(arg, known_models=known_models)
+    return known_models
+
+
+def get_flat_models_from_field(
+    field: ModelField, known_models: TypeModelSet
+) -> TypeModelSet:
+    field_type = field.type_
+    if lenient_issubclass(field_type, BaseModel):
+        if field_type in known_models:
+            return known_models
+        known_models.add(field_type)
+        get_flat_models_from_model(field_type, known_models=known_models)
+    elif lenient_issubclass(field_type, Enum):
+        known_models.add(field_type)
+    else:
+        get_flat_models_from_annotation(field_type, known_models=known_models)
+    return known_models
+
+
+def get_flat_models_from_fields(
+    fields: Sequence[ModelField], known_models: TypeModelSet
+) -> TypeModelSet:
+    for field in fields:
+        get_flat_models_from_field(field, known_models=known_models)
+    return known_models
+
+
+def get_long_model_name(model: TypeModelOrEnum) -> str:
+    return f"{model.__module__}__{model.__qualname__}".replace(".", "__")
index cf8406b0fcc23477ab8b12c26977d971bac0ccf1..34185b96aa96116c81d6220003a9c50f38ea3c34 100644 (file)
@@ -11,11 +11,9 @@ from typing import (
 )
 
 from fastapi._compat import (
-    PYDANTIC_V2,
     CoreSchema,
     GetJsonSchemaHandler,
     JsonSchemaValue,
-    with_info_plain_validator_function,
 )
 from starlette.datastructures import URL as URL  # noqa: F401
 from starlette.datastructures import Address as Address  # noqa: F401
@@ -154,11 +152,10 @@ class UploadFile(StarletteUploadFile):
             raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
         return cast(UploadFile, __input_value)
 
-    if not PYDANTIC_V2:
-
-        @classmethod
-        def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
-            field_schema.update({"type": "string", "format": "binary"})
+    # TODO: remove when deprecating Pydantic v1
+    @classmethod
+    def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+        field_schema.update({"type": "string", "format": "binary"})
 
     @classmethod
     def __get_pydantic_json_schema__(
@@ -170,6 +167,8 @@ class UploadFile(StarletteUploadFile):
     def __get_pydantic_core_schema__(
         cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
     ) -> CoreSchema:
+        from ._compat.v2 import with_info_plain_validator_function
+
         return with_info_plain_validator_function(cls._validate)
 
 
index e49380cb30028260d6f0aa841ef7156324b7ee75..675ad6faffc26ae349e514692c85de4daeb2f49e 100644 (file)
@@ -23,11 +23,11 @@ import anyio
 from fastapi import params
 from fastapi._compat import (
     PYDANTIC_V2,
-    ErrorWrapper,
     ModelField,
     RequiredParam,
     Undefined,
-    _regenerate_error_with_loc,
+    _is_error_wrapper,
+    _is_model_class,
     copy_field_info,
     create_body_model,
     evaluate_forwardref,
@@ -45,8 +45,10 @@ from fastapi._compat import (
     lenient_issubclass,
     sequence_types,
     serialize_sequence_value,
+    v1,
     value_is_sequence,
 )
+from fastapi._compat.shared import annotation_is_pydantic_v1
 from fastapi.background import BackgroundTasks
 from fastapi.concurrency import (
     asynccontextmanager,
@@ -74,6 +76,8 @@ from starlette.responses import Response
 from starlette.websockets import WebSocket
 from typing_extensions import Annotated, get_args, get_origin
 
+from .. import temp_pydantic_v1_params
+
 if sys.version_info >= (3, 13):  # pragma: no cover
     from inspect import iscoroutinefunction
 else:  # pragma: no cover
@@ -219,7 +223,7 @@ def _get_flat_fields_from_params(fields: List[ModelField]) -> List[ModelField]:
     if not fields:
         return fields
     first_field = fields[0]
-    if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
+    if len(fields) == 1 and _is_model_class(first_field.type_):
         fields_to_extract = get_cached_model_fields(first_field.type_)
         return fields_to_extract
     return fields
@@ -315,7 +319,9 @@ def get_dependant(
             )
             continue
         assert param_details.field is not None
-        if isinstance(param_details.field.field_info, params.Body):
+        if isinstance(
+            param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body)
+        ):
             dependant.body_params.append(param_details.field)
         else:
             add_param_to_fields(field=param_details.field, dependant=dependant)
@@ -374,28 +380,38 @@ def analyze_param(
         fastapi_annotations = [
             arg
             for arg in annotated_args[1:]
-            if isinstance(arg, (FieldInfo, params.Depends))
+            if isinstance(arg, (FieldInfo, v1.FieldInfo, params.Depends))
         ]
         fastapi_specific_annotations = [
             arg
             for arg in fastapi_annotations
-            if isinstance(arg, (params.Param, params.Body, params.Depends))
+            if isinstance(
+                arg,
+                (
+                    params.Param,
+                    temp_pydantic_v1_params.Param,
+                    params.Body,
+                    temp_pydantic_v1_params.Body,
+                    params.Depends,
+                ),
+            )
         ]
         if fastapi_specific_annotations:
-            fastapi_annotation: Union[FieldInfo, params.Depends, None] = (
+            fastapi_annotation: Union[FieldInfo, v1.FieldInfo, params.Depends, None] = (
                 fastapi_specific_annotations[-1]
             )
         else:
             fastapi_annotation = None
         # Set default for Annotated FieldInfo
-        if isinstance(fastapi_annotation, FieldInfo):
+        if isinstance(fastapi_annotation, (FieldInfo, v1.FieldInfo)):
             # Copy `field_info` because we mutate `field_info.default` below.
             field_info = copy_field_info(
                 field_info=fastapi_annotation, annotation=use_annotation
             )
-            assert (
-                field_info.default is Undefined or field_info.default is RequiredParam
-            ), (
+            assert field_info.default in {
+                Undefined,
+                v1.Undefined,
+            } or field_info.default in {RequiredParam, v1.RequiredParam}, (
                 f"`{field_info.__class__.__name__}` default value cannot be set in"
                 f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
             )
@@ -419,14 +435,15 @@ def analyze_param(
         )
         depends = value
     # Get FieldInfo from default value
-    elif isinstance(value, FieldInfo):
+    elif isinstance(value, (FieldInfo, v1.FieldInfo)):
         assert field_info is None, (
             "Cannot specify FastAPI annotations in `Annotated` and default value"
             f" together for {param_name!r}"
         )
         field_info = value
         if PYDANTIC_V2:
-            field_info.annotation = type_annotation
+            if isinstance(field_info, FieldInfo):
+                field_info.annotation = type_annotation
 
     # Get Depends from type annotation
     if depends is not None and depends.dependency is None:
@@ -463,7 +480,14 @@ def analyze_param(
         ) or is_uploadfile_sequence_annotation(type_annotation):
             field_info = params.File(annotation=use_annotation, default=default_value)
         elif not field_annotation_is_scalar(annotation=type_annotation):
-            field_info = params.Body(annotation=use_annotation, default=default_value)
+            if annotation_is_pydantic_v1(use_annotation):
+                field_info = temp_pydantic_v1_params.Body(
+                    annotation=use_annotation, default=default_value
+                )
+            else:
+                field_info = params.Body(
+                    annotation=use_annotation, default=default_value
+                )
         else:
             field_info = params.Query(annotation=use_annotation, default=default_value)
 
@@ -472,12 +496,14 @@ def analyze_param(
     if field_info is not None:
         # Handle field_info.in_
         if is_path_param:
-            assert isinstance(field_info, params.Path), (
+            assert isinstance(
+                field_info, (params.Path, temp_pydantic_v1_params.Path)
+            ), (
                 f"Cannot use `{field_info.__class__.__name__}` for path param"
                 f" {param_name!r}"
             )
         elif (
-            isinstance(field_info, params.Param)
+            isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param))
             and getattr(field_info, "in_", None) is None
         ):
             field_info.in_ = params.ParamTypes.query
@@ -486,7 +512,7 @@ def analyze_param(
             field_info,
             param_name,
         )
-        if isinstance(field_info, params.Form):
+        if isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form)):
             ensure_multipart_is_installed()
         if not field_info.alias and getattr(field_info, "convert_underscores", None):
             alias = param_name.replace("_", "-")
@@ -498,19 +524,19 @@ def analyze_param(
             type_=use_annotation_from_field_info,
             default=field_info.default,
             alias=alias,
-            required=field_info.default in (RequiredParam, Undefined),
+            required=field_info.default in (RequiredParam, v1.RequiredParam, Undefined),
             field_info=field_info,
         )
         if is_path_param:
             assert is_scalar_field(field=field), (
                 "Path params must be of one of the supported types"
             )
-        elif isinstance(field_info, params.Query):
+        elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)):
             assert (
                 is_scalar_field(field)
                 or is_scalar_sequence_field(field)
                 or (
-                    lenient_issubclass(field.type_, BaseModel)
+                    _is_model_class(field.type_)
                     # For Pydantic v1
                     and getattr(field, "shape", 1) == 1
                 )
@@ -712,10 +738,10 @@ def _validate_value_with_model_field(
         else:
             return deepcopy(field.default), []
     v_, errors_ = field.validate(value, values, loc=loc)
-    if isinstance(errors_, ErrorWrapper):
+    if _is_error_wrapper(errors_):  # type: ignore[arg-type]
         return None, [errors_]
     elif isinstance(errors_, list):
-        new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
+        new_errors = v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
         return None, new_errors
     else:
         return v_, []
@@ -732,7 +758,7 @@ def _get_multidict_value(
     if (
         value is None
         or (
-            isinstance(field.field_info, params.Form)
+            isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form))
             and isinstance(value, str)  # For type checks
             and value == ""
         )
@@ -798,7 +824,7 @@ def request_params_to_args(
 
     if single_not_embedded_field:
         field_info = first_field.field_info
-        assert isinstance(field_info, params.Param), (
+        assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
             "Params must be subclasses of Param"
         )
         loc: Tuple[str, ...] = (field_info.in_.value,)
@@ -810,7 +836,7 @@ def request_params_to_args(
     for field in fields:
         value = _get_multidict_value(field, received_params)
         field_info = field.field_info
-        assert isinstance(field_info, params.Param), (
+        assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
             "Params must be subclasses of Param"
         )
         loc = (field_info.in_.value, field.alias)
@@ -837,7 +863,7 @@ def is_union_of_base_models(field_type: Any) -> bool:
     union_args = get_args(field_type)
 
     for arg in union_args:
-        if not lenient_issubclass(arg, BaseModel):
+        if not _is_model_class(arg):
             return False
 
     return True
@@ -859,8 +885,8 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool:
     # If it's a Form (or File) field, it has to be a BaseModel (or a union of BaseModels) to be top level
     # otherwise it has to be embedded, so that the key value pair can be extracted
     if (
-        isinstance(first_field.field_info, params.Form)
-        and not lenient_issubclass(first_field.type_, BaseModel)
+        isinstance(first_field.field_info, (params.Form, temp_pydantic_v1_params.Form))
+        and not _is_model_class(first_field.type_)
         and not is_union_of_base_models(first_field.type_)
     ):
         return True
@@ -877,14 +903,14 @@ async def _extract_form_body(
         value = _get_multidict_value(field, received_body)
         field_info = field.field_info
         if (
-            isinstance(field_info, params.File)
+            isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
             and is_bytes_field(field)
             and isinstance(value, UploadFile)
         ):
             value = await value.read()
         elif (
             is_bytes_sequence_field(field)
-            and isinstance(field_info, params.File)
+            and isinstance(field_info, (params.File, temp_pydantic_v1_params.File))
             and value_is_sequence(value)
         ):
             # For types
@@ -925,7 +951,7 @@ async def request_body_to_args(
 
     if (
         single_not_embedded_field
-        and lenient_issubclass(first_field.type_, BaseModel)
+        and _is_model_class(first_field.type_)
         and isinstance(received_body, FormData)
     ):
         fields_to_extract = get_cached_model_fields(first_field.type_)
@@ -990,15 +1016,28 @@ def get_body_field(
         BodyFieldInfo_kwargs["default"] = None
     if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
         BodyFieldInfo: Type[params.Body] = params.File
+    elif any(
+        isinstance(f.field_info, temp_pydantic_v1_params.File)
+        for f in flat_dependant.body_params
+    ):
+        BodyFieldInfo: Type[temp_pydantic_v1_params.Body] = temp_pydantic_v1_params.File  # type: ignore[no-redef]
     elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
         BodyFieldInfo = params.Form
+    elif any(
+        isinstance(f.field_info, temp_pydantic_v1_params.Form)
+        for f in flat_dependant.body_params
+    ):
+        BodyFieldInfo = temp_pydantic_v1_params.Form  # type: ignore[assignment]
     else:
-        BodyFieldInfo = params.Body
+        if annotation_is_pydantic_v1(BodyModel):
+            BodyFieldInfo = temp_pydantic_v1_params.Body  # type: ignore[assignment]
+        else:
+            BodyFieldInfo = params.Body
 
         body_param_media_types = [
             f.field_info.media_type
             for f in flat_dependant.body_params
-            if isinstance(f.field_info, params.Body)
+            if isinstance(f.field_info, (params.Body, temp_pydantic_v1_params.Body))
         ]
         if len(set(body_param_media_types)) == 1:
             BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
index b037f8bb5eff4017d9f51e12c955fd84e92d0933..8ff7d58dd5ae39c28344f15ab0d4a0db1768be25 100644 (file)
@@ -17,6 +17,7 @@ from types import GeneratorType
 from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
 from uuid import UUID
 
+from fastapi._compat import v1
 from fastapi.types import IncEx
 from pydantic import BaseModel
 from pydantic.color import Color
@@ -24,7 +25,7 @@ from pydantic.networks import AnyUrl, NameEmail
 from pydantic.types import SecretBytes, SecretStr
 from typing_extensions import Annotated, Doc
 
-from ._compat import PYDANTIC_V2, UndefinedType, Url, _model_dump
+from ._compat import Url, _is_undefined, _model_dump
 
 
 # Taken from Pydantic v1 as is
@@ -58,6 +59,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
 ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
     bytes: lambda o: o.decode(),
     Color: str,
+    v1.Color: str,
     datetime.date: isoformat,
     datetime.datetime: isoformat,
     datetime.time: isoformat,
@@ -74,14 +76,19 @@ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
     IPv6Interface: str,
     IPv6Network: str,
     NameEmail: str,
+    v1.NameEmail: str,
     Path: str,
     Pattern: lambda o: o.pattern,
     SecretBytes: str,
+    v1.SecretBytes: str,
     SecretStr: str,
+    v1.SecretStr: str,
     set: list,
     UUID: str,
     Url: str,
+    v1.Url: str,
     AnyUrl: str,
+    v1.AnyUrl: str,
 }
 
 
@@ -213,10 +220,10 @@ def jsonable_encoder(
         include = set(include)
     if exclude is not None and not isinstance(exclude, (set, dict)):
         exclude = set(exclude)
-    if isinstance(obj, BaseModel):
+    if isinstance(obj, (BaseModel, v1.BaseModel)):
         # TODO: remove when deprecating Pydantic v1
         encoders: Dict[Any, Any] = {}
-        if not PYDANTIC_V2:
+        if isinstance(obj, v1.BaseModel):
             encoders = getattr(obj.__config__, "json_encoders", {})  # type: ignore[attr-defined]
             if custom_encoder:
                 encoders = {**encoders, **custom_encoder}
@@ -260,7 +267,7 @@ def jsonable_encoder(
         return str(obj)
     if isinstance(obj, (str, int, float, type(None))):
         return obj
-    if isinstance(obj, UndefinedType):
+    if _is_undefined(obj):
         return None
     if isinstance(obj, dict):
         encoded_dict = {}
index 21105cf654fb92a88081d21ae3530550ec3eb36d..dbc93d28920e41f534ff0e84f2146f4a5de99a60 100644 (file)
@@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union,
 
 from fastapi import routing
 from fastapi._compat import (
-    GenerateJsonSchema,
     JsonSchemaValue,
     ModelField,
     Undefined,
@@ -22,7 +21,7 @@ from fastapi.dependencies.utils import (
     get_flat_params,
 )
 from fastapi.encoders import jsonable_encoder
-from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
+from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
 from fastapi.openapi.models import OpenAPI
 from fastapi.params import Body, ParamTypes
 from fastapi.responses import Response
@@ -37,6 +36,8 @@ from starlette.responses import JSONResponse
 from starlette.routing import BaseRoute
 from typing_extensions import Literal
 
+from .._compat import _is_model_field
+
 validation_error_definition = {
     "title": "ValidationError",
     "type": "object",
@@ -94,7 +95,6 @@ def get_openapi_security_definitions(
 def _get_openapi_operation_parameters(
     *,
     dependant: Dependant,
-    schema_generator: GenerateJsonSchema,
     model_name_map: ModelNameMap,
     field_mapping: Dict[
         Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
@@ -128,7 +128,6 @@ def _get_openapi_operation_parameters(
                 continue
             param_schema = get_schema_from_model_field(
                 field=param,
-                schema_generator=schema_generator,
                 model_name_map=model_name_map,
                 field_mapping=field_mapping,
                 separate_input_output_schemas=separate_input_output_schemas,
@@ -169,7 +168,6 @@ def _get_openapi_operation_parameters(
 def get_openapi_operation_request_body(
     *,
     body_field: Optional[ModelField],
-    schema_generator: GenerateJsonSchema,
     model_name_map: ModelNameMap,
     field_mapping: Dict[
         Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
@@ -178,10 +176,9 @@ def get_openapi_operation_request_body(
 ) -> Optional[Dict[str, Any]]:
     if not body_field:
         return None
-    assert isinstance(body_field, ModelField)
+    assert _is_model_field(body_field)
     body_schema = get_schema_from_model_field(
         field=body_field,
-        schema_generator=schema_generator,
         model_name_map=model_name_map,
         field_mapping=field_mapping,
         separate_input_output_schemas=separate_input_output_schemas,
@@ -254,7 +251,6 @@ def get_openapi_path(
     *,
     route: routing.APIRoute,
     operation_ids: Set[str],
-    schema_generator: GenerateJsonSchema,
     model_name_map: ModelNameMap,
     field_mapping: Dict[
         Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
@@ -287,7 +283,6 @@ def get_openapi_path(
                 security_schemes.update(security_definitions)
             operation_parameters = _get_openapi_operation_parameters(
                 dependant=route.dependant,
-                schema_generator=schema_generator,
                 model_name_map=model_name_map,
                 field_mapping=field_mapping,
                 separate_input_output_schemas=separate_input_output_schemas,
@@ -309,7 +304,6 @@ def get_openapi_path(
             if method in METHODS_WITH_BODY:
                 request_body_oai = get_openapi_operation_request_body(
                     body_field=route.body_field,
-                    schema_generator=schema_generator,
                     model_name_map=model_name_map,
                     field_mapping=field_mapping,
                     separate_input_output_schemas=separate_input_output_schemas,
@@ -327,7 +321,6 @@ def get_openapi_path(
                         ) = get_openapi_path(
                             route=callback,
                             operation_ids=operation_ids,
-                            schema_generator=schema_generator,
                             model_name_map=model_name_map,
                             field_mapping=field_mapping,
                             separate_input_output_schemas=separate_input_output_schemas,
@@ -358,7 +351,6 @@ def get_openapi_path(
                     if route.response_field:
                         response_schema = get_schema_from_model_field(
                             field=route.response_field,
-                            schema_generator=schema_generator,
                             model_name_map=model_name_map,
                             field_mapping=field_mapping,
                             separate_input_output_schemas=separate_input_output_schemas,
@@ -392,7 +384,6 @@ def get_openapi_path(
                     if field:
                         additional_field_schema = get_schema_from_model_field(
                             field=field,
-                            schema_generator=schema_generator,
                             model_name_map=model_name_map,
                             field_mapping=field_mapping,
                             separate_input_output_schemas=separate_input_output_schemas,
@@ -454,7 +445,7 @@ def get_fields_from_routes(
             route, routing.APIRoute
         ):
             if route.body_field:
-                assert isinstance(route.body_field, ModelField), (
+                assert _is_model_field(route.body_field), (
                     "A request body must be a Pydantic Field"
                 )
                 body_fields_from_routes.append(route.body_field)
@@ -510,10 +501,8 @@ def get_openapi(
     operation_ids: Set[str] = set()
     all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or []))
     model_name_map = get_compat_model_name_map(all_fields)
-    schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
     field_mapping, definitions = get_definitions(
         fields=all_fields,
-        schema_generator=schema_generator,
         model_name_map=model_name_map,
         separate_input_output_schemas=separate_input_output_schemas,
     )
@@ -522,7 +511,6 @@ def get_openapi(
             result = get_openapi_path(
                 route=route,
                 operation_ids=operation_ids,
-                schema_generator=schema_generator,
                 model_name_map=model_name_map,
                 field_mapping=field_mapping,
                 separate_input_output_schemas=separate_input_output_schemas,
@@ -542,7 +530,6 @@ def get_openapi(
             result = get_openapi_path(
                 route=webhook,
                 operation_ids=operation_ids,
-                schema_generator=schema_generator,
                 model_name_map=model_name_map,
                 field_mapping=field_mapping,
                 separate_input_output_schemas=separate_input_output_schemas,
index 65f739d9559d3194f81ac5d06c56ec0a76a947a5..fe25d7dec773ec2f031a458f81f043b43ad3b207 100644 (file)
@@ -24,7 +24,7 @@ from typing import (
     Union,
 )
 
-from fastapi import params
+from fastapi import params, temp_pydantic_v1_params
 from fastapi._compat import (
     ModelField,
     Undefined,
@@ -307,7 +307,9 @@ def get_request_handler(
 ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
     assert dependant.call is not None, "dependant.call must be a function"
     is_coroutine = iscoroutinefunction(dependant.call)
-    is_body_form = body_field and isinstance(body_field.field_info, params.Form)
+    is_body_form = body_field and isinstance(
+        body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)
+    )
     if isinstance(response_class, DefaultPlaceholder):
         actual_response_class: Type[Response] = response_class.value
     else:
diff --git a/fastapi/temp_pydantic_v1_params.py b/fastapi/temp_pydantic_v1_params.py
new file mode 100644 (file)
index 0000000..0535ee7
--- /dev/null
@@ -0,0 +1,724 @@
+import warnings
+from typing import Any, Callable, Dict, List, Optional, Union
+
+from fastapi.openapi.models import Example
+from fastapi.params import ParamTypes
+from typing_extensions import Annotated, deprecated
+
+from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE
+from ._compat.v1 import FieldInfo, Undefined
+
+_Unset: Any = Undefined
+
+
+class Param(FieldInfo):  # type: ignore[misc]
+    in_: ParamTypes
+
+    def __init__(
+        self,
+        default: Any = Undefined,
+        *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
+        alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        gt: Optional[float] = None,
+        ge: Optional[float] = None,
+        lt: Optional[float] = None,
+        le: Optional[float] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
+        examples: Optional[List[Any]] = None,
+        example: Annotated[
+            Optional[Any],
+            deprecated(
+                "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
+                "although still supported. Use examples instead."
+            ),
+        ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
+        deprecated: Union[deprecated, str, bool, None] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
+        **extra: Any,
+    ):
+        if example is not _Unset:
+            warnings.warn(
+                "`example` has been deprecated, please use `examples` instead",
+                category=DeprecationWarning,
+                stacklevel=4,
+            )
+        self.example = example
+        self.include_in_schema = include_in_schema
+        self.openapi_examples = openapi_examples
+        kwargs = dict(
+            default=default,
+            default_factory=default_factory,
+            alias=alias,
+            title=title,
+            description=description,
+            gt=gt,
+            ge=ge,
+            lt=lt,
+            le=le,
+            min_length=min_length,
+            max_length=max_length,
+            discriminator=discriminator,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            **extra,
+        )
+        if examples is not None:
+            kwargs["examples"] = examples
+        if regex is not None:
+            warnings.warn(
+                "`regex` has been deprecated, please use `pattern` instead",
+                category=DeprecationWarning,
+                stacklevel=4,
+            )
+        current_json_schema_extra = json_schema_extra or extra
+        if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
+            self.deprecated = deprecated
+        else:
+            kwargs["deprecated"] = deprecated
+        kwargs["regex"] = pattern or regex
+        kwargs.update(**current_json_schema_extra)
+        use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
+
+        super().__init__(**use_kwargs)
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}({self.default})"
+
+
+class Path(Param):  # type: ignore[misc]
+    in_ = ParamTypes.path
+
+    def __init__(
+        self,
+        default: Any = ...,
+        *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
+        alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        gt: Optional[float] = None,
+        ge: Optional[float] = None,
+        lt: Optional[float] = None,
+        le: Optional[float] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
+        examples: Optional[List[Any]] = None,
+        example: Annotated[
+            Optional[Any],
+            deprecated(
+                "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
+                "although still supported. Use examples instead."
+            ),
+        ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
+        deprecated: Union[deprecated, str, bool, None] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
+        **extra: Any,
+    ):
+        assert default is ..., "Path parameters cannot have a default value"
+        self.in_ = self.in_
+        super().__init__(
+            default=default,
+            default_factory=default_factory,
+            annotation=annotation,
+            alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
+            title=title,
+            description=description,
+            gt=gt,
+            ge=ge,
+            lt=lt,
+            le=le,
+            min_length=min_length,
+            max_length=max_length,
+            pattern=pattern,
+            regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            deprecated=deprecated,
+            example=example,
+            examples=examples,
+            openapi_examples=openapi_examples,
+            include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
+            **extra,
+        )
+
+
+class Query(Param):  # type: ignore[misc]
+    in_ = ParamTypes.query
+
+    def __init__(
+        self,
+        default: Any = Undefined,
+        *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
+        alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        gt: Optional[float] = None,
+        ge: Optional[float] = None,
+        lt: Optional[float] = None,
+        le: Optional[float] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
+        examples: Optional[List[Any]] = None,
+        example: Annotated[
+            Optional[Any],
+            deprecated(
+                "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
+                "although still supported. Use examples instead."
+            ),
+        ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
+        deprecated: Union[deprecated, str, bool, None] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
+        **extra: Any,
+    ):
+        super().__init__(
+            default=default,
+            default_factory=default_factory,
+            annotation=annotation,
+            alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
+            title=title,
+            description=description,
+            gt=gt,
+            ge=ge,
+            lt=lt,
+            le=le,
+            min_length=min_length,
+            max_length=max_length,
+            pattern=pattern,
+            regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            deprecated=deprecated,
+            example=example,
+            examples=examples,
+            openapi_examples=openapi_examples,
+            include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
+            **extra,
+        )
+
+
+class Header(Param):  # type: ignore[misc]
+    in_ = ParamTypes.header
+
+    def __init__(
+        self,
+        default: Any = Undefined,
+        *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
+        alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
+        convert_underscores: bool = True,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        gt: Optional[float] = None,
+        ge: Optional[float] = None,
+        lt: Optional[float] = None,
+        le: Optional[float] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
+        examples: Optional[List[Any]] = None,
+        example: Annotated[
+            Optional[Any],
+            deprecated(
+                "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
+                "although still supported. Use examples instead."
+            ),
+        ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
+        deprecated: Union[deprecated, str, bool, None] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
+        **extra: Any,
+    ):
+        self.convert_underscores = convert_underscores
+        super().__init__(
+            default=default,
+            default_factory=default_factory,
+            annotation=annotation,
+            alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
+            title=title,
+            description=description,
+            gt=gt,
+            ge=ge,
+            lt=lt,
+            le=le,
+            min_length=min_length,
+            max_length=max_length,
+            pattern=pattern,
+            regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            deprecated=deprecated,
+            example=example,
+            examples=examples,
+            openapi_examples=openapi_examples,
+            include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
+            **extra,
+        )
+
+
+class Cookie(Param):  # type: ignore[misc]
+    in_ = ParamTypes.cookie
+
+    def __init__(
+        self,
+        default: Any = Undefined,
+        *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
+        alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        gt: Optional[float] = None,
+        ge: Optional[float] = None,
+        lt: Optional[float] = None,
+        le: Optional[float] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
+        examples: Optional[List[Any]] = None,
+        example: Annotated[
+            Optional[Any],
+            deprecated(
+                "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
+                "although still supported. Use examples instead."
+            ),
+        ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
+        deprecated: Union[deprecated, str, bool, None] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
+        **extra: Any,
+    ):
+        super().__init__(
+            default=default,
+            default_factory=default_factory,
+            annotation=annotation,
+            alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
+            title=title,
+            description=description,
+            gt=gt,
+            ge=ge,
+            lt=lt,
+            le=le,
+            min_length=min_length,
+            max_length=max_length,
+            pattern=pattern,
+            regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            deprecated=deprecated,
+            example=example,
+            examples=examples,
+            openapi_examples=openapi_examples,
+            include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
+            **extra,
+        )
+
+
+class Body(FieldInfo):  # type: ignore[misc]
+    def __init__(
+        self,
+        default: Any = Undefined,
+        *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
+        embed: Union[bool, None] = None,
+        media_type: str = "application/json",
+        alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        gt: Optional[float] = None,
+        ge: Optional[float] = None,
+        lt: Optional[float] = None,
+        le: Optional[float] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
+        examples: Optional[List[Any]] = None,
+        example: Annotated[
+            Optional[Any],
+            deprecated(
+                "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
+                "although still supported. Use examples instead."
+            ),
+        ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
+        deprecated: Union[deprecated, str, bool, None] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
+        **extra: Any,
+    ):
+        self.embed = embed
+        self.media_type = media_type
+        if example is not _Unset:
+            warnings.warn(
+                "`example` has been deprecated, please use `examples` instead",
+                category=DeprecationWarning,
+                stacklevel=4,
+            )
+        self.example = example
+        self.include_in_schema = include_in_schema
+        self.openapi_examples = openapi_examples
+        kwargs = dict(
+            default=default,
+            default_factory=default_factory,
+            alias=alias,
+            title=title,
+            description=description,
+            gt=gt,
+            ge=ge,
+            lt=lt,
+            le=le,
+            min_length=min_length,
+            max_length=max_length,
+            discriminator=discriminator,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            **extra,
+        )
+        if examples is not None:
+            kwargs["examples"] = examples
+        if regex is not None:
+            warnings.warn(
+                "`regex` has been deprecated, please use `pattern` instead",
+                category=DeprecationWarning,
+                stacklevel=4,
+            )
+        current_json_schema_extra = json_schema_extra or extra
+        if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
+            self.deprecated = deprecated
+        else:
+            kwargs["deprecated"] = deprecated
+        kwargs["regex"] = pattern or regex
+        kwargs.update(**current_json_schema_extra)
+
+        use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
+
+        super().__init__(**use_kwargs)
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}({self.default})"
+
+
+class Form(Body):  # type: ignore[misc]
+    def __init__(
+        self,
+        default: Any = Undefined,
+        *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
+        media_type: str = "application/x-www-form-urlencoded",
+        alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        gt: Optional[float] = None,
+        ge: Optional[float] = None,
+        lt: Optional[float] = None,
+        le: Optional[float] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
+        examples: Optional[List[Any]] = None,
+        example: Annotated[
+            Optional[Any],
+            deprecated(
+                "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
+                "although still supported. Use examples instead."
+            ),
+        ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
+        deprecated: Union[deprecated, str, bool, None] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
+        **extra: Any,
+    ):
+        super().__init__(
+            default=default,
+            default_factory=default_factory,
+            annotation=annotation,
+            media_type=media_type,
+            alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
+            title=title,
+            description=description,
+            gt=gt,
+            ge=ge,
+            lt=lt,
+            le=le,
+            min_length=min_length,
+            max_length=max_length,
+            pattern=pattern,
+            regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            deprecated=deprecated,
+            example=example,
+            examples=examples,
+            openapi_examples=openapi_examples,
+            include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
+            **extra,
+        )
+
+
+class File(Form):  # type: ignore[misc]
+    def __init__(
+        self,
+        default: Any = Undefined,
+        *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
+        media_type: str = "multipart/form-data",
+        alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
+        title: Optional[str] = None,
+        description: Optional[str] = None,
+        gt: Optional[float] = None,
+        ge: Optional[float] = None,
+        lt: Optional[float] = None,
+        le: Optional[float] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
+        examples: Optional[List[Any]] = None,
+        example: Annotated[
+            Optional[Any],
+            deprecated(
+                "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
+                "although still supported. Use examples instead."
+            ),
+        ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
+        deprecated: Union[deprecated, str, bool, None] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
+        **extra: Any,
+    ):
+        super().__init__(
+            default=default,
+            default_factory=default_factory,
+            annotation=annotation,
+            media_type=media_type,
+            alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
+            title=title,
+            description=description,
+            gt=gt,
+            ge=ge,
+            lt=lt,
+            le=le,
+            min_length=min_length,
+            max_length=max_length,
+            pattern=pattern,
+            regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            deprecated=deprecated,
+            example=example,
+            examples=examples,
+            openapi_examples=openapi_examples,
+            include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
+            **extra,
+        )
index 98725ff196bf0dc409214a536c214e56fbddf7bc..3ea9271b1170aca34bf9480c406636a5bfa568ea 100644 (file)
@@ -23,10 +23,12 @@ from fastapi._compat import (
     Undefined,
     UndefinedType,
     Validator,
+    annotation_is_pydantic_v1,
     lenient_issubclass,
+    v1,
 )
 from fastapi.datastructures import DefaultPlaceholder, DefaultType
-from pydantic import BaseModel, create_model
+from pydantic import BaseModel
 from pydantic.fields import FieldInfo
 from typing_extensions import Literal
 
@@ -60,50 +62,70 @@ def get_path_param_names(path: str) -> Set[str]:
     return set(re.findall("{(.*?)}", path))
 
 
+_invalid_args_message = (
+    "Invalid args for response field! Hint: "
+    "check that {type_} is a valid Pydantic field type. "
+    "If you are using a return type annotation that is not a valid Pydantic "
+    "field (e.g. Union[Response, dict, None]) you can disable generating the "
+    "response model from the type annotation with the path operation decorator "
+    "parameter response_model=None. Read more: "
+    "https://fastapi.tiangolo.com/tutorial/response-model/"
+)
+
+
 def create_model_field(
     name: str,
     type_: Any,
     class_validators: Optional[Dict[str, Validator]] = None,
     default: Optional[Any] = Undefined,
     required: Union[bool, UndefinedType] = Undefined,
-    model_config: Type[BaseConfig] = BaseConfig,
+    model_config: Union[Type[BaseConfig], None] = None,
     field_info: Optional[FieldInfo] = None,
     alias: Optional[str] = None,
     mode: Literal["validation", "serialization"] = "validation",
+    version: Literal["1", "auto"] = "auto",
 ) -> ModelField:
     class_validators = class_validators or {}
-    if PYDANTIC_V2:
+
+    v1_model_config = v1.BaseConfig
+    v1_field_info = field_info or v1.FieldInfo()
+    v1_kwargs = {
+        "name": name,
+        "field_info": v1_field_info,
+        "type_": type_,
+        "class_validators": class_validators,
+        "default": default,
+        "required": required,
+        "model_config": v1_model_config,
+        "alias": alias,
+    }
+
+    if (
+        annotation_is_pydantic_v1(type_)
+        or isinstance(field_info, v1.FieldInfo)
+        or version == "1"
+    ):
+        try:
+            return v1.ModelField(**v1_kwargs)  # type: ignore[no-any-return]
+        except RuntimeError:
+            raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
+    elif PYDANTIC_V2:
+        from ._compat import v2
+
         field_info = field_info or FieldInfo(
             annotation=type_, default=default, alias=alias
         )
-    else:
-        field_info = field_info or FieldInfo()
-    kwargs = {"name": name, "field_info": field_info}
-    if PYDANTIC_V2:
-        kwargs.update({"mode": mode})
-    else:
-        kwargs.update(
-            {
-                "type_": type_,
-                "class_validators": class_validators,
-                "default": default,
-                "required": required,
-                "model_config": model_config,
-                "alias": alias,
-            }
-        )
+        kwargs = {"mode": mode, "name": name, "field_info": field_info}
+        try:
+            return v2.ModelField(**kwargs)  # type: ignore[return-value,arg-type]
+        except PydanticSchemaGenerationError:
+            raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
+    # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
+    # a Pydantic v1 type, like a constrained int
     try:
-        return ModelField(**kwargs)  # type: ignore[arg-type]
-    except (RuntimeError, PydanticSchemaGenerationError):
-        raise fastapi.exceptions.FastAPIError(
-            "Invalid args for response field! Hint: "
-            f"check that {type_} is a valid Pydantic field type. "
-            "If you are using a return type annotation that is not a valid Pydantic "
-            "field (e.g. Union[Response, dict, None]) you can disable generating the "
-            "response model from the type annotation with the path operation decorator "
-            "parameter response_model=None. Read more: "
-            "https://fastapi.tiangolo.com/tutorial/response-model/"
-        ) from None
+        return v1.ModelField(**v1_kwargs)  # type: ignore[no-any-return]
+    except RuntimeError:
+        raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
 
 
 def create_cloned_field(
@@ -112,7 +134,10 @@ def create_cloned_field(
     cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
 ) -> ModelField:
     if PYDANTIC_V2:
-        return field
+        from ._compat import v2
+
+        if isinstance(field, v2.ModelField):
+            return field
     # cloned_types caches already cloned types to support recursive models and improve
     # performance by avoiding unnecessary cloning
     if cloned_types is None:
@@ -122,17 +147,18 @@ def create_cloned_field(
     if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
         original_type = original_type.__pydantic_model__
     use_type = original_type
-    if lenient_issubclass(original_type, BaseModel):
-        original_type = cast(Type[BaseModel], original_type)
+    if lenient_issubclass(original_type, v1.BaseModel):
+        original_type = cast(Type[v1.BaseModel], original_type)
         use_type = cloned_types.get(original_type)
         if use_type is None:
-            use_type = create_model(original_type.__name__, __base__=original_type)
+            use_type = v1.create_model(original_type.__name__, __base__=original_type)
             cloned_types[original_type] = use_type
             for f in original_type.__fields__.values():
                 use_type.__fields__[f.name] = create_cloned_field(
-                    f, cloned_types=cloned_types
+                    f,
+                    cloned_types=cloned_types,
                 )
-    new_field = create_model_field(name=field.name, type_=use_type)
+    new_field = create_model_field(name=field.name, type_=use_type, version="1")
     new_field.has_alias = field.has_alias  # type: ignore[attr-defined]
     new_field.alias = field.alias  # type: ignore[misc]
     new_field.class_validators = field.class_validators  # type: ignore[attr-defined]
index 43c6864896eb5888dee311a94edeaa070477a587..f79dbdabcb9d995e8fb18968951ddd6dd7337b46 100644 (file)
@@ -2,53 +2,45 @@ from typing import Any, Dict, List, Union
 
 from fastapi import FastAPI, UploadFile
 from fastapi._compat import (
-    ModelField,
     Undefined,
     _get_model_config,
     get_cached_model_fields,
-    get_model_fields,
-    is_bytes_sequence_annotation,
     is_scalar_field,
     is_uploadfile_sequence_annotation,
+    v1,
 )
+from fastapi._compat.shared import is_bytes_sequence_annotation
 from fastapi.testclient import TestClient
-from pydantic import BaseConfig, BaseModel, ConfigDict
+from pydantic import BaseModel, ConfigDict
 from pydantic.fields import FieldInfo
 
-from .utils import needs_pydanticv1, needs_pydanticv2
+from .utils import needs_py_lt_314, needs_pydanticv2
 
 
 @needs_pydanticv2
 def test_model_field_default_required():
+    from fastapi._compat import v2
+
     # For coverage
     field_info = FieldInfo(annotation=str)
-    field = ModelField(name="foo", field_info=field_info)
+    field = v2.ModelField(name="foo", field_info=field_info)
     assert field.default is Undefined
 
 
-@needs_pydanticv1
-def test_upload_file_dummy_with_info_plain_validator_function():
+def test_v1_plain_validator_function():
     # For coverage
-    assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {}
+    def func(v):  # pragma: no cover
+        return v
+
+    result = v1.with_info_plain_validator_function(func)
+    assert result == {}
 
 
-@needs_pydanticv1
-def test_union_scalar_list():
+def test_is_model_field():
     # For coverage
-    # TODO: there might not be a current valid code path that uses this, it would
-    # potentially enable query parameters defined as both a scalar and a list
-    # but that would require more refactors, also not sure it's really useful
-    from fastapi._compat import is_pv1_scalar_field
-
-    field_info = FieldInfo()
-    field = ModelField(
-        name="foo",
-        field_info=field_info,
-        type_=Union[str, List[int]],
-        class_validators={},
-        model_config=BaseConfig,
-    )
-    assert not is_pv1_scalar_field(field)
+    from fastapi._compat import _is_model_field
+
+    assert not _is_model_field(str)
 
 
 @needs_pydanticv2
@@ -141,21 +133,22 @@ def test_is_uploadfile_sequence_annotation():
     assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
 
 
+@needs_py_lt_314
 def test_is_pv1_scalar_field():
     # For coverage
-    class Model(BaseModel):
+    class Model(v1.BaseModel):
         foo: Union[str, Dict[str, Any]]
 
-    fields = get_model_fields(Model)
+    fields = v1.get_model_fields(Model)
     assert not is_scalar_field(fields[0])
 
 
 def test_get_model_fields_cached():
-    class Model(BaseModel):
+    class Model(v1.BaseModel):
         foo: str
 
-    non_cached_fields = get_model_fields(Model)
-    non_cached_fields2 = get_model_fields(Model)
+    non_cached_fields = v1.get_model_fields(Model)
+    non_cached_fields2 = v1.get_model_fields(Model)
     cached_fields = get_cached_model_fields(Model)
     cached_fields2 = get_cached_model_fields(Model)
     for f1, f2 in zip(cached_fields, cached_fields2):
diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py
new file mode 100644 (file)
index 0000000..7064761
--- /dev/null
@@ -0,0 +1,1122 @@
+import sys
+from typing import List, Optional
+
+import pytest
+
+from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+from fastapi import FastAPI
+from fastapi._compat.v1 import BaseModel
+from fastapi.temp_pydantic_v1_params import (
+    Body,
+    Cookie,
+    File,
+    Form,
+    Header,
+    Path,
+    Query,
+)
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from typing_extensions import Annotated
+
+
+class Item(BaseModel):
+    name: str
+    price: float
+    description: Optional[str] = None
+
+
+app = FastAPI()
+
+
+@app.get("/items/{item_id}")
+def get_item_with_path(
+    item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)],
+):
+    return {"item_id": item_id}
+
+
+@app.get("/items/")
+def get_items_with_query(
+    q: Annotated[
+        Optional[str], Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$")
+    ] = None,
+    skip: Annotated[int, Query(ge=0)] = 0,
+    limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10,
+):
+    return {"q": q, "skip": skip, "limit": limit}
+
+
+@app.get("/users/")
+def get_user_with_header(
+    x_custom: Annotated[Optional[str], Header()] = None,
+    x_token: Annotated[Optional[str], Header(convert_underscores=True)] = None,
+):
+    return {"x_custom": x_custom, "x_token": x_token}
+
+
+@app.get("/cookies/")
+def get_cookies(
+    session_id: Annotated[Optional[str], Cookie()] = None,
+    tracking_id: Annotated[Optional[str], Cookie(min_length=10)] = None,
+):
+    return {"session_id": session_id, "tracking_id": tracking_id}
+
+
+@app.post("/items/")
+def create_item(
+    item: Annotated[
+        Item,
+        Body(examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]),
+    ],
+):
+    return {"item": item}
+
+
+@app.post("/items-embed/")
+def create_item_embed(
+    item: Annotated[Item, Body(embed=True)],
+):
+    return {"item": item}
+
+
+@app.put("/items/{item_id}")
+def update_item(
+    item_id: Annotated[int, Path(ge=1)],
+    item: Annotated[Item, Body()],
+    importance: Annotated[int, Body(gt=0, le=10)],
+):
+    return {"item": item, "importance": importance}
+
+
+@app.post("/form-data/")
+def submit_form(
+    username: Annotated[str, Form(min_length=3, max_length=50)],
+    password: Annotated[str, Form(min_length=8)],
+    email: Annotated[Optional[str], Form()] = None,
+):
+    return {"username": username, "password": password, "email": email}
+
+
+@app.post("/upload/")
+def upload_file(
+    file: Annotated[bytes, File()],
+    description: Annotated[Optional[str], Form()] = None,
+):
+    return {"file_size": len(file), "description": description}
+
+
+@app.post("/upload-multiple/")
+def upload_multiple_files(
+    files: Annotated[List[bytes], File()],
+    note: Annotated[str, Form()] = "",
+):
+    return {
+        "file_count": len(files),
+        "total_size": sum(len(f) for f in files),
+        "note": note,
+    }
+
+
+client = TestClient(app)
+
+
+# Path parameter tests
+def test_path_param_valid():
+    response = client.get("/items/50")
+    assert response.status_code == 200
+    assert response.json() == {"item_id": 50}
+
+
+def test_path_param_too_large():
+    response = client.get("/items/1001")
+    assert response.status_code == 422
+    error = response.json()["detail"][0]
+    assert error["loc"] == ["path", "item_id"]
+
+
+def test_path_param_too_small():
+    response = client.get("/items/0")
+    assert response.status_code == 422
+    error = response.json()["detail"][0]
+    assert error["loc"] == ["path", "item_id"]
+
+
+# Query parameter tests
+def test_query_params_valid():
+    response = client.get("/items/?q=test search&skip=5&limit=20")
+    assert response.status_code == 200
+    assert response.json() == {"q": "test search", "skip": 5, "limit": 20}
+
+
+def test_query_params_defaults():
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"q": None, "skip": 0, "limit": 10}
+
+
+def test_query_param_too_short():
+    response = client.get("/items/?q=ab")
+    assert response.status_code == 422
+    error = response.json()["detail"][0]
+    assert error["loc"] == ["query", "q"]
+
+
+def test_query_param_invalid_pattern():
+    response = client.get("/items/?q=test@#$")
+    assert response.status_code == 422
+    error = response.json()["detail"][0]
+    assert error["loc"] == ["query", "q"]
+
+
+def test_query_param_limit_too_large():
+    response = client.get("/items/?limit=101")
+    assert response.status_code == 422
+    error = response.json()["detail"][0]
+    assert error["loc"] == ["query", "limit"]
+
+
+# Header parameter tests
+def test_header_params():
+    response = client.get(
+        "/users/",
+        headers={"X-Custom": "Plumbus", "X-Token": "secret-token"},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "x_custom": "Plumbus",
+        "x_token": "secret-token",
+    }
+
+
+def test_header_underscore_conversion():
+    response = client.get(
+        "/users/",
+        headers={"x-token": "secret-token-with-dash"},
+    )
+    assert response.status_code == 200
+    assert response.json()["x_token"] == "secret-token-with-dash"
+
+
+def test_header_params_none():
+    response = client.get("/users/")
+    assert response.status_code == 200
+    assert response.json() == {"x_custom": None, "x_token": None}
+
+
+# Cookie parameter tests
+def test_cookie_params():
+    with TestClient(app) as client:
+        client.cookies.set("session_id", "abc123")
+        client.cookies.set("tracking_id", "1234567890abcdef")
+        response = client.get("/cookies/")
+    assert response.status_code == 200
+    assert response.json() == {
+        "session_id": "abc123",
+        "tracking_id": "1234567890abcdef",
+    }
+
+
+def test_cookie_tracking_id_too_short():
+    with TestClient(app) as client:
+        client.cookies.set("tracking_id", "short")
+        response = client.get("/cookies/")
+    assert response.status_code == 422
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["cookie", "tracking_id"],
+                    "msg": "ensure this value has at least 10 characters",
+                    "type": "value_error.any_str.min_length",
+                    "ctx": {"limit_value": 10},
+                }
+            ]
+        }
+    )
+
+
+def test_cookie_params_none():
+    response = client.get("/cookies/")
+    assert response.status_code == 200
+    assert response.json() == {"session_id": None, "tracking_id": None}
+
+
+# Body parameter tests
+def test_body_param():
+    response = client.post(
+        "/items/",
+        json={"name": "Test Item", "price": 29.99, "description": "A test item"},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item": {
+            "name": "Test Item",
+            "price": 29.99,
+            "description": "A test item",
+        }
+    }
+
+
+def test_body_param_minimal():
+    response = client.post(
+        "/items/",
+        json={"name": "Minimal", "price": 9.99},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item": {"name": "Minimal", "price": 9.99, "description": None}
+    }
+
+
+def test_body_param_missing_required():
+    response = client.post(
+        "/items/",
+        json={"name": "Incomplete"},
+    )
+    assert response.status_code == 422
+    error = response.json()["detail"][0]
+    assert error["loc"] == ["body", "price"]
+
+
+def test_body_embed():
+    response = client.post(
+        "/items-embed/",
+        json={"item": {"name": "Embedded", "price": 15.0}},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item": {"name": "Embedded", "price": 15.0, "description": None}
+    }
+
+
+def test_body_embed_wrong_structure():
+    response = client.post(
+        "/items-embed/",
+        json={"name": "Not Embedded", "price": 15.0},
+    )
+    assert response.status_code == 422
+
+
+# Multiple body parameters test
+def test_multiple_body_params():
+    response = client.put(
+        "/items/5",
+        json={
+            "item": {"name": "Updated Item", "price": 49.99},
+            "importance": 8,
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == snapshot(
+        {
+            "item": {"name": "Updated Item", "price": 49.99, "description": None},
+            "importance": 8,
+        }
+    )
+
+
+def test_multiple_body_params_importance_too_large():
+    response = client.put(
+        "/items/5",
+        json={
+            "item": {"name": "Item", "price": 10.0},
+            "importance": 11,
+        },
+    )
+    assert response.status_code == 422
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "importance"],
+                    "msg": "ensure this value is less than or equal to 10",
+                    "type": "value_error.number.not_le",
+                    "ctx": {"limit_value": 10},
+                }
+            ]
+        }
+    )
+
+
+def test_multiple_body_params_importance_too_small():
+    response = client.put(
+        "/items/5",
+        json={
+            "item": {"name": "Item", "price": 10.0},
+            "importance": 0,
+        },
+    )
+    assert response.status_code == 422
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "importance"],
+                    "msg": "ensure this value is greater than 0",
+                    "type": "value_error.number.not_gt",
+                    "ctx": {"limit_value": 0},
+                }
+            ]
+        }
+    )
+
+
+# Form parameter tests
+def test_form_data_valid():
+    response = client.post(
+        "/form-data/",
+        data={
+            "username": "testuser",
+            "password": "password123",
+            "email": "test@example.com",
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "username": "testuser",
+        "password": "password123",
+        "email": "test@example.com",
+    }
+
+
+def test_form_data_optional_field():
+    response = client.post(
+        "/form-data/",
+        data={"username": "testuser", "password": "password123"},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "username": "testuser",
+        "password": "password123",
+        "email": None,
+    }
+
+
+def test_form_data_username_too_short():
+    response = client.post(
+        "/form-data/",
+        data={"username": "ab", "password": "password123"},
+    )
+    assert response.status_code == 422
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "username"],
+                    "msg": "ensure this value has at least 3 characters",
+                    "type": "value_error.any_str.min_length",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_form_data_password_too_short():
+    response = client.post(
+        "/form-data/",
+        data={"username": "testuser", "password": "short"},
+    )
+    assert response.status_code == 422
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "password"],
+                    "msg": "ensure this value has at least 8 characters",
+                    "type": "value_error.any_str.min_length",
+                    "ctx": {"limit_value": 8},
+                }
+            ]
+        }
+    )
+
+
+# File upload tests
+def test_upload_file():
+    response = client.post(
+        "/upload/",
+        files={"file": ("test.txt", b"Hello, World!", "text/plain")},
+        data={"description": "A test file"},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "file_size": 13,
+        "description": "A test file",
+    }
+
+
+def test_upload_file_without_description():
+    response = client.post(
+        "/upload/",
+        files={"file": ("test.txt", b"Hello!", "text/plain")},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "file_size": 6,
+        "description": None,
+    }
+
+
+def test_upload_multiple_files():
+    response = client.post(
+        "/upload-multiple/",
+        files=[
+            ("files", ("file1.txt", b"Content 1", "text/plain")),
+            ("files", ("file2.txt", b"Content 2", "text/plain")),
+            ("files", ("file3.txt", b"Content 3", "text/plain")),
+        ],
+        data={"note": "Multiple files uploaded"},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "file_count": 3,
+        "total_size": 27,
+        "note": "Multiple files uploaded",
+    }
+
+
+def test_upload_multiple_files_empty_note():
+    response = client.post(
+        "/upload-multiple/",
+        files=[
+            ("files", ("file1.txt", b"Test", "text/plain")),
+        ],
+    )
+    assert response.status_code == 200
+    assert response.json()["file_count"] == 1
+    assert response.json()["note"] == ""
+
+
+# __repr__ tests
+def test_query_repr():
+    query_param = Query(default=None, min_length=3)
+    assert repr(query_param) == "Query(None)"
+
+
+def test_body_repr():
+    body_param = Body(default=None)
+    assert repr(body_param) == "Body(None)"
+
+
+# Deprecation warning tests for regex parameter
+def test_query_regex_deprecation_warning():
+    with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"):
+        Query(regex="^test$")
+
+
+def test_body_regex_deprecation_warning():
+    with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"):
+        Body(regex="^test$")
+
+
+# Deprecation warning tests for example parameter
+def test_query_example_deprecation_warning():
+    with pytest.warns(DeprecationWarning, match="`example` has been deprecated"):
+        Query(example="test example")
+
+
+def test_body_example_deprecation_warning():
+    with pytest.warns(DeprecationWarning, match="`example` has been deprecated"):
+        Body(example={"test": "example"})
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/items/{item_id}": {
+                    "get": {
+                        "summary": "Get Item With Path",
+                        "operationId": "get_item_with_path_items__item_id__get",
+                        "parameters": [
+                            {
+                                "name": "item_id",
+                                "in": "path",
+                                "required": True,
+                                "schema": {
+                                    "title": "The ID of the item",
+                                    "minimum": 1,
+                                    "maximum": 1000,
+                                    "type": "integer",
+                                },
+                            }
+                        ],
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    },
+                    "put": {
+                        "summary": "Update Item",
+                        "operationId": "update_item_items__item_id__put",
+                        "parameters": [
+                            {
+                                "name": "item_id",
+                                "in": "path",
+                                "required": True,
+                                "schema": {
+                                    "title": "Item Id",
+                                    "minimum": 1,
+                                    "type": "integer",
+                                },
+                            }
+                        ],
+                        "requestBody": {
+                            "required": True,
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/Body_update_item_items__item_id__put"
+                                            }
+                                        ),
+                                        v2=snapshot(
+                                            {
+                                                "title": "Body",
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Body_update_item_items__item_id__put"
+                                                    }
+                                                ],
+                                            }
+                                        ),
+                                    ),
+                                }
+                            },
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    },
+                },
+                "/items/": {
+                    "get": {
+                        "summary": "Get Items With Query",
+                        "operationId": "get_items_with_query_items__get",
+                        "parameters": [
+                            {
+                                "name": "q",
+                                "in": "query",
+                                "required": False,
+                                "schema": {
+                                    "title": "Q",
+                                    "maxLength": 50,
+                                    "minLength": 3,
+                                    "pattern": "^[a-zA-Z0-9 ]+$",
+                                    "type": "string",
+                                },
+                            },
+                            {
+                                "name": "skip",
+                                "in": "query",
+                                "required": False,
+                                "schema": {
+                                    "title": "Skip",
+                                    "default": 0,
+                                    "minimum": 0,
+                                    "type": "integer",
+                                },
+                            },
+                            {
+                                "name": "limit",
+                                "in": "query",
+                                "required": False,
+                                "schema": {
+                                    "title": "Limit",
+                                    "default": 10,
+                                    "minimum": 1,
+                                    "maximum": 100,
+                                    "examples": [5],
+                                    "type": "integer",
+                                },
+                            },
+                        ],
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    },
+                    "post": {
+                        "summary": "Create Item",
+                        "operationId": "create_item_items__post",
+                        "requestBody": {
+                            "required": True,
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "title": "Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "price": 35.4,
+                                                "description": "The Foo item",
+                                            }
+                                        ],
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                    }
+                                }
+                            },
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    },
+                },
+                "/users/": {
+                    "get": {
+                        "summary": "Get User With Header",
+                        "operationId": "get_user_with_header_users__get",
+                        "parameters": [
+                            {
+                                "name": "x-custom",
+                                "in": "header",
+                                "required": False,
+                                "schema": {"title": "X-Custom", "type": "string"},
+                            },
+                            {
+                                "name": "x-token",
+                                "in": "header",
+                                "required": False,
+                                "schema": {"title": "X-Token", "type": "string"},
+                            },
+                        ],
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/cookies/": {
+                    "get": {
+                        "summary": "Get Cookies",
+                        "operationId": "get_cookies_cookies__get",
+                        "parameters": [
+                            {
+                                "name": "session_id",
+                                "in": "cookie",
+                                "required": False,
+                                "schema": {"title": "Session Id", "type": "string"},
+                            },
+                            {
+                                "name": "tracking_id",
+                                "in": "cookie",
+                                "required": False,
+                                "schema": {
+                                    "title": "Tracking Id",
+                                    "minLength": 10,
+                                    "type": "string",
+                                },
+                            },
+                        ],
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/items-embed/": {
+                    "post": {
+                        "summary": "Create Item Embed",
+                        "operationId": "create_item_embed_items_embed__post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post"
+                                            }
+                                        ),
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post"
+                                                    }
+                                                ],
+                                                "title": "Body",
+                                            }
+                                        ),
+                                    ),
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/form-data/": {
+                    "post": {
+                        "summary": "Submit Form",
+                        "operationId": "submit_form_form_data__post",
+                        "requestBody": {
+                            "content": {
+                                "application/x-www-form-urlencoded": {
+                                    "schema": pydantic_snapshot(
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/Body_submit_form_form_data__post"
+                                            }
+                                        ),
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Body_submit_form_form_data__post"
+                                                    }
+                                                ],
+                                                "title": "Body",
+                                            }
+                                        ),
+                                    ),
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/upload/": {
+                    "post": {
+                        "summary": "Upload File",
+                        "operationId": "upload_file_upload__post",
+                        "requestBody": {
+                            "content": {
+                                "multipart/form-data": {
+                                    "schema": pydantic_snapshot(
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/Body_upload_file_upload__post"
+                                            }
+                                        ),
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Body_upload_file_upload__post"
+                                                    }
+                                                ],
+                                                "title": "Body",
+                                            }
+                                        ),
+                                    ),
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/upload-multiple/": {
+                    "post": {
+                        "summary": "Upload Multiple Files",
+                        "operationId": "upload_multiple_files_upload_multiple__post",
+                        "requestBody": {
+                            "content": {
+                                "multipart/form-data": {
+                                    "schema": pydantic_snapshot(
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post"
+                                            }
+                                        ),
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post"
+                                                    }
+                                                ],
+                                                "title": "Body",
+                                            }
+                                        ),
+                                    ),
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+            },
+            "components": {
+                "schemas": {
+                    "Body_create_item_embed_items_embed__post": {
+                        "properties": pydantic_snapshot(
+                            v1=snapshot(
+                                {"item": {"$ref": "#/components/schemas/Item"}}
+                            ),
+                            v2=snapshot(
+                                {
+                                    "item": {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                }
+                            ),
+                        ),
+                        "type": "object",
+                        "required": ["item"],
+                        "title": "Body_create_item_embed_items_embed__post",
+                    },
+                    "Body_submit_form_form_data__post": {
+                        "properties": {
+                            "username": {
+                                "type": "string",
+                                "maxLength": 50,
+                                "minLength": 3,
+                                "title": "Username",
+                            },
+                            "password": {
+                                "type": "string",
+                                "minLength": 8,
+                                "title": "Password",
+                            },
+                            "email": {"type": "string", "title": "Email"},
+                        },
+                        "type": "object",
+                        "required": ["username", "password"],
+                        "title": "Body_submit_form_form_data__post",
+                    },
+                    "Body_update_item_items__item_id__put": {
+                        "properties": {
+                            "item": pydantic_snapshot(
+                                v1=snapshot({"$ref": "#/components/schemas/Item"}),
+                                v2=snapshot(
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                ),
+                            ),
+                            "importance": {
+                                "type": "integer",
+                                "maximum": 10.0,
+                                "exclusiveMinimum": 0.0,
+                                "title": "Importance",
+                            },
+                        },
+                        "type": "object",
+                        "required": ["item", "importance"],
+                        "title": "Body_update_item_items__item_id__put",
+                    },
+                    "Body_upload_file_upload__post": {
+                        "properties": {
+                            "file": {
+                                "type": "string",
+                                "format": "binary",
+                                "title": "File",
+                            },
+                            "description": {"type": "string", "title": "Description"},
+                        },
+                        "type": "object",
+                        "required": ["file"],
+                        "title": "Body_upload_file_upload__post",
+                    },
+                    "Body_upload_multiple_files_upload_multiple__post": {
+                        "properties": {
+                            "files": {
+                                "items": {"type": "string", "format": "binary"},
+                                "type": "array",
+                                "title": "Files",
+                            },
+                            "note": {"type": "string", "title": "Note", "default": ""},
+                        },
+                        "type": "object",
+                        "required": ["files"],
+                        "title": "Body_upload_multiple_files_upload_multiple__post",
+                    },
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
+                        },
+                        "type": "object",
+                        "title": "HTTPValidationError",
+                    },
+                    "Item": {
+                        "properties": {
+                            "name": {"type": "string", "title": "Name"},
+                            "price": {"type": "number", "title": "Price"},
+                            "description": {"type": "string", "title": "Description"},
+                        },
+                        "type": "object",
+                        "required": ["name", "price"],
+                        "title": "Item",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
+                            },
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
+                        },
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                    },
+                }
+            },
+        }
+    )
index f77195dc5f88d2ae1397e9cedfb5c9a9e281c822..439e6d4484335a80a3be707101c03c43f72cf27b 100644 (file)
@@ -5,6 +5,7 @@ import fastapi.openapi.utils
 import pydantic.schema
 import pytest
 from fastapi import FastAPI
+from fastapi._compat import v1
 from pydantic import BaseModel
 from starlette.testclient import TestClient
 
@@ -166,14 +167,12 @@ def test_model_description_escaped_with_formfeed(sort_reversed: bool):
     """
     all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes)
 
-    flat_models = fastapi._compat.get_flat_models_from_fields(
-        all_fields, known_models=set()
-    )
+    flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set())
     model_name_map = pydantic.schema.get_model_name_map(flat_models)
 
     expected_address_description = "This is a public description of an Address\n"
 
-    models = fastapi._compat.get_model_definitions(
+    models = v1.get_model_definitions(
         flat_models=SortedTypeSet(flat_models, sort_reversed=sort_reversed),
         model_name_map=model_name_map,
     )
index f7e045259faf7afbd713f6a2f4d505157185e977..fa73620eae379d88850b1ea80ae4194342541612 100644 (file)
@@ -2,6 +2,7 @@ from typing import List, Optional
 
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
 from pydantic import BaseModel
 
 from .utils import PYDANTIC_V2, needs_pydanticv2
@@ -135,217 +136,223 @@ def test_openapi_schema():
     client = get_app_client()
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
-    assert response.json() == {
-        "openapi": "3.1.0",
-        "info": {"title": "FastAPI", "version": "0.1.0"},
-        "paths": {
-            "/items/": {
-                "get": {
-                    "summary": "Read Items",
-                    "operationId": "read_items_items__get",
-                    "responses": {
-                        "200": {
-                            "description": "Successful Response",
-                            "content": {
-                                "application/json": {
-                                    "schema": {
-                                        "items": {
-                                            "$ref": "#/components/schemas/Item-Output"
-                                        },
-                                        "type": "array",
-                                        "title": "Response Read Items Items  Get",
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/items/": {
+                    "get": {
+                        "summary": "Read Items",
+                        "operationId": "read_items_items__get",
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/Item-Output"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Read Items Items  Get",
+                                        }
                                     }
-                                }
-                            },
-                        }
-                    },
-                },
-                "post": {
-                    "summary": "Create Item",
-                    "operationId": "create_item_items__post",
-                    "requestBody": {
-                        "content": {
-                            "application/json": {
-                                "schema": {"$ref": "#/components/schemas/Item-Input"}
+                                },
                             }
                         },
-                        "required": True,
                     },
-                    "responses": {
-                        "200": {
-                            "description": "Successful Response",
+                    "post": {
+                        "summary": "Create Item",
+                        "operationId": "create_item_items__post",
+                        "requestBody": {
                             "content": {
                                 "application/json": {
                                     "schema": {
-                                        "$ref": "#/components/schemas/Item-Output"
+                                        "$ref": "#/components/schemas/Item-Input"
                                     }
                                 }
                             },
+                            "required": True,
                         },
-                        "402": {
-                            "description": "Payment Required",
-                            "content": {
-                                "application/json": {
-                                    "schema": {
-                                        "$ref": "#/components/schemas/Item-Output"
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/Item-Output"
+                                        }
                                     }
-                                }
+                                },
                             },
-                        },
-                        "422": {
-                            "description": "Validation Error",
-                            "content": {
-                                "application/json": {
-                                    "schema": {
-                                        "$ref": "#/components/schemas/HTTPValidationError"
+                            "402": {
+                                "description": "Payment Required",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/Item-Output"
+                                        }
                                     }
-                                }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
                             },
                         },
                     },
                 },
-            },
-            "/items-list/": {
-                "post": {
-                    "summary": "Create Item List",
-                    "operationId": "create_item_list_items_list__post",
-                    "requestBody": {
-                        "content": {
-                            "application/json": {
-                                "schema": {
-                                    "items": {
-                                        "$ref": "#/components/schemas/Item-Input"
-                                    },
-                                    "type": "array",
-                                    "title": "Item",
-                                }
-                            }
-                        },
-                        "required": True,
-                    },
-                    "responses": {
-                        "200": {
-                            "description": "Successful Response",
-                            "content": {"application/json": {"schema": {}}},
-                        },
-                        "422": {
-                            "description": "Validation Error",
+                "/items-list/": {
+                    "post": {
+                        "summary": "Create Item List",
+                        "operationId": "create_item_list_items_list__post",
+                        "requestBody": {
                             "content": {
                                 "application/json": {
                                     "schema": {
-                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                        "items": {
+                                            "$ref": "#/components/schemas/Item-Input"
+                                        },
+                                        "type": "array",
+                                        "title": "Item",
                                     }
                                 }
                             },
+                            "required": True,
                         },
-                    },
-                }
-            },
-        },
-        "components": {
-            "schemas": {
-                "HTTPValidationError": {
-                    "properties": {
-                        "detail": {
-                            "items": {"$ref": "#/components/schemas/ValidationError"},
-                            "type": "array",
-                            "title": "Detail",
-                        }
-                    },
-                    "type": "object",
-                    "title": "HTTPValidationError",
-                },
-                "Item-Input": {
-                    "properties": {
-                        "name": {"type": "string", "title": "Name"},
-                        "description": {
-                            "anyOf": [{"type": "string"}, {"type": "null"}],
-                            "title": "Description",
-                        },
-                        "sub": {
-                            "anyOf": [
-                                {"$ref": "#/components/schemas/SubItem-Input"},
-                                {"type": "null"},
-                            ]
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
                         },
-                    },
-                    "type": "object",
-                    "required": ["name"],
-                    "title": "Item",
+                    }
                 },
-                "Item-Output": {
-                    "properties": {
-                        "name": {"type": "string", "title": "Name"},
-                        "description": {
-                            "anyOf": [{"type": "string"}, {"type": "null"}],
-                            "title": "Description",
-                        },
-                        "sub": {
-                            "anyOf": [
-                                {"$ref": "#/components/schemas/SubItem-Output"},
-                                {"type": "null"},
-                            ]
+            },
+            "components": {
+                "schemas": {
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
                         },
+                        "type": "object",
+                        "title": "HTTPValidationError",
                     },
-                    "type": "object",
-                    "required": ["name", "description", "sub"],
-                    "title": "Item",
-                },
-                "SubItem-Input": {
-                    "properties": {
-                        "subname": {"type": "string", "title": "Subname"},
-                        "sub_description": {
-                            "anyOf": [{"type": "string"}, {"type": "null"}],
-                            "title": "Sub Description",
+                    "Item-Input": {
+                        "properties": {
+                            "name": {"type": "string", "title": "Name"},
+                            "description": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Description",
+                            },
+                            "sub": {
+                                "anyOf": [
+                                    {"$ref": "#/components/schemas/SubItem-Input"},
+                                    {"type": "null"},
+                                ]
+                            },
                         },
-                        "tags": {
-                            "items": {"type": "string"},
-                            "type": "array",
-                            "title": "Tags",
-                            "default": [],
+                        "type": "object",
+                        "required": ["name"],
+                        "title": "Item",
+                    },
+                    "Item-Output": {
+                        "properties": {
+                            "name": {"type": "string", "title": "Name"},
+                            "description": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Description",
+                            },
+                            "sub": {
+                                "anyOf": [
+                                    {"$ref": "#/components/schemas/SubItem-Output"},
+                                    {"type": "null"},
+                                ]
+                            },
                         },
+                        "type": "object",
+                        "required": ["name", "description", "sub"],
+                        "title": "Item",
                     },
-                    "type": "object",
-                    "required": ["subname"],
-                    "title": "SubItem",
-                },
-                "SubItem-Output": {
-                    "properties": {
-                        "subname": {"type": "string", "title": "Subname"},
-                        "sub_description": {
-                            "anyOf": [{"type": "string"}, {"type": "null"}],
-                            "title": "Sub Description",
+                    "SubItem-Input": {
+                        "properties": {
+                            "subname": {"type": "string", "title": "Subname"},
+                            "sub_description": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Sub Description",
+                            },
+                            "tags": {
+                                "items": {"type": "string"},
+                                "type": "array",
+                                "title": "Tags",
+                                "default": [],
+                            },
                         },
-                        "tags": {
-                            "items": {"type": "string"},
-                            "type": "array",
-                            "title": "Tags",
-                            "default": [],
+                        "type": "object",
+                        "required": ["subname"],
+                        "title": "SubItem",
+                    },
+                    "SubItem-Output": {
+                        "properties": {
+                            "subname": {"type": "string", "title": "Subname"},
+                            "sub_description": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Sub Description",
+                            },
+                            "tags": {
+                                "items": {"type": "string"},
+                                "type": "array",
+                                "title": "Tags",
+                                "default": [],
+                            },
                         },
+                        "type": "object",
+                        "required": ["subname", "sub_description", "tags"],
+                        "title": "SubItem",
                     },
-                    "type": "object",
-                    "required": ["subname", "sub_description", "tags"],
-                    "title": "SubItem",
-                },
-                "ValidationError": {
-                    "properties": {
-                        "loc": {
-                            "items": {
-                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
                             },
-                            "type": "array",
-                            "title": "Location",
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
                         },
-                        "msg": {"type": "string", "title": "Message"},
-                        "type": {"type": "string", "title": "Error Type"},
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
                     },
-                    "type": "object",
-                    "required": ["loc", "msg", "type"],
-                    "title": "ValidationError",
-                },
-            }
-        },
-    }
+                }
+            },
+        }
+    )
 
 
 @needs_pydanticv2
diff --git a/tests/test_pydantic_v1_v2_01.py b/tests/test_pydantic_v1_v2_01.py
new file mode 100644 (file)
index 0000000..769e5fa
--- /dev/null
@@ -0,0 +1,475 @@
+import sys
+from typing import Any, List, Union
+
+from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+from fastapi import FastAPI
+from fastapi._compat.v1 import BaseModel
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+
+
+class SubItem(BaseModel):
+    name: str
+
+
+class Item(BaseModel):
+    title: str
+    size: int
+    description: Union[str, None] = None
+    sub: SubItem
+    multi: List[SubItem] = []
+
+
+app = FastAPI()
+
+
+@app.post("/simple-model")
+def handle_simple_model(data: SubItem) -> SubItem:
+    return data
+
+
+@app.post("/simple-model-filter", response_model=SubItem)
+def handle_simple_model_filter(data: SubItem) -> Any:
+    extended_data = data.dict()
+    extended_data.update({"secret_price": 42})
+    return extended_data
+
+
+@app.post("/item")
+def handle_item(data: Item) -> Item:
+    return data
+
+
+@app.post("/item-filter", response_model=Item)
+def handle_item_filter(data: Item) -> Any:
+    extended_data = data.dict()
+    extended_data.update({"secret_data": "classified", "internal_id": 12345})
+    extended_data["sub"].update({"internal_id": 67890})
+    return extended_data
+
+
+client = TestClient(app)
+
+
+def test_old_simple_model():
+    response = client.post(
+        "/simple-model",
+        json={"name": "Foo"},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {"name": "Foo"}
+
+
+def test_old_simple_model_validation_error():
+    response = client.post(
+        "/simple-model",
+        json={"wrong_name": "Foo"},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "name"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
+
+
+def test_old_simple_model_filter():
+    response = client.post(
+        "/simple-model-filter",
+        json={"name": "Foo"},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {"name": "Foo"}
+
+
+def test_item_model():
+    response = client.post(
+        "/item",
+        json={
+            "title": "Test Item",
+            "size": 100,
+            "description": "This is a test item",
+            "sub": {"name": "SubItem1"},
+            "multi": [{"name": "Multi1"}, {"name": "Multi2"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "Test Item",
+        "size": 100,
+        "description": "This is a test item",
+        "sub": {"name": "SubItem1"},
+        "multi": [{"name": "Multi1"}, {"name": "Multi2"}],
+    }
+
+
+def test_item_model_minimal():
+    response = client.post(
+        "/item",
+        json={"title": "Minimal Item", "size": 50, "sub": {"name": "SubMin"}},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "Minimal Item",
+        "size": 50,
+        "description": None,
+        "sub": {"name": "SubMin"},
+        "multi": [],
+    }
+
+
+def test_item_model_validation_errors():
+    response = client.post(
+        "/item",
+        json={"title": "Missing fields"},
+    )
+    assert response.status_code == 422, response.text
+    error_detail = response.json()["detail"]
+    assert len(error_detail) == 2
+    assert {
+        "loc": ["body", "size"],
+        "msg": "field required",
+        "type": "value_error.missing",
+    } in error_detail
+    assert {
+        "loc": ["body", "sub"],
+        "msg": "field required",
+        "type": "value_error.missing",
+    } in error_detail
+
+
+def test_item_model_nested_validation_error():
+    response = client.post(
+        "/item",
+        json={"title": "Test Item", "size": 100, "sub": {"wrong_field": "test"}},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "sub", "name"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
+
+
+def test_item_model_invalid_type():
+    response = client.post(
+        "/item",
+        json={"title": "Test Item", "size": "not_a_number", "sub": {"name": "SubItem"}},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "size"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                }
+            ]
+        }
+    )
+
+
+def test_item_filter():
+    response = client.post(
+        "/item-filter",
+        json={
+            "title": "Filtered Item",
+            "size": 200,
+            "description": "Test filtering",
+            "sub": {"name": "SubFiltered"},
+            "multi": [],
+        },
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result == {
+        "title": "Filtered Item",
+        "size": 200,
+        "description": "Test filtering",
+        "sub": {"name": "SubFiltered"},
+        "multi": [],
+    }
+    assert "secret_data" not in result
+    assert "internal_id" not in result
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/simple-model": {
+                    "post": {
+                        "summary": "Handle Simple Model",
+                        "operationId": "handle_simple_model_simple_model_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/SubItem"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/SubItem"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/SubItem"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/simple-model-filter": {
+                    "post": {
+                        "summary": "Handle Simple Model Filter",
+                        "operationId": "handle_simple_model_filter_simple_model_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/SubItem"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/SubItem"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/SubItem"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/item": {
+                    "post": {
+                        "summary": "Handle Item",
+                        "operationId": "handle_item_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/item-filter": {
+                    "post": {
+                        "summary": "Handle Item Filter",
+                        "operationId": "handle_item_filter_item_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+            },
+            "components": {
+                "schemas": {
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
+                        },
+                        "type": "object",
+                        "title": "HTTPValidationError",
+                    },
+                    "Item": {
+                        "properties": {
+                            "title": {"type": "string", "title": "Title"},
+                            "size": {"type": "integer", "title": "Size"},
+                            "description": {"type": "string", "title": "Description"},
+                            "sub": {"$ref": "#/components/schemas/SubItem"},
+                            "multi": {
+                                "items": {"$ref": "#/components/schemas/SubItem"},
+                                "type": "array",
+                                "title": "Multi",
+                                "default": [],
+                            },
+                        },
+                        "type": "object",
+                        "required": ["title", "size", "sub"],
+                        "title": "Item",
+                    },
+                    "SubItem": {
+                        "properties": {"name": {"type": "string", "title": "Name"}},
+                        "type": "object",
+                        "required": ["name"],
+                        "title": "SubItem",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
+                            },
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
+                        },
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                    },
+                }
+            },
+        }
+    )
diff --git a/tests/test_pydantic_v1_v2_list.py b/tests/test_pydantic_v1_v2_list.py
new file mode 100644 (file)
index 0000000..64f3dd3
--- /dev/null
@@ -0,0 +1,701 @@
+import sys
+from typing import Any, List, Union
+
+from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+from fastapi import FastAPI
+from fastapi._compat.v1 import BaseModel
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+
+
+class SubItem(BaseModel):
+    name: str
+
+
+class Item(BaseModel):
+    title: str
+    size: int
+    description: Union[str, None] = None
+    sub: SubItem
+    multi: List[SubItem] = []
+
+
+app = FastAPI()
+
+
+@app.post("/item")
+def handle_item(data: Item) -> List[Item]:
+    return [data, data]
+
+
+@app.post("/item-filter", response_model=List[Item])
+def handle_item_filter(data: Item) -> Any:
+    extended_data = data.dict()
+    extended_data.update({"secret_data": "classified", "internal_id": 12345})
+    extended_data["sub"].update({"internal_id": 67890})
+    return [extended_data, extended_data]
+
+
+@app.post("/item-list")
+def handle_item_list(data: List[Item]) -> Item:
+    if data:
+        return data[0]
+    return Item(title="", size=0, sub=SubItem(name=""))
+
+
+@app.post("/item-list-filter", response_model=Item)
+def handle_item_list_filter(data: List[Item]) -> Any:
+    if data:
+        extended_data = data[0].dict()
+        extended_data.update({"secret_data": "classified", "internal_id": 12345})
+        extended_data["sub"].update({"internal_id": 67890})
+        return extended_data
+    return Item(title="", size=0, sub=SubItem(name=""))
+
+
+@app.post("/item-list-to-list")
+def handle_item_list_to_list(data: List[Item]) -> List[Item]:
+    return data
+
+
+@app.post("/item-list-to-list-filter", response_model=List[Item])
+def handle_item_list_to_list_filter(data: List[Item]) -> Any:
+    if data:
+        extended_data = data[0].dict()
+        extended_data.update({"secret_data": "classified", "internal_id": 12345})
+        extended_data["sub"].update({"internal_id": 67890})
+        return [extended_data, extended_data]
+    return []
+
+
+client = TestClient(app)
+
+
+def test_item_to_list():
+    response = client.post(
+        "/item",
+        json={
+            "title": "Test Item",
+            "size": 100,
+            "description": "This is a test item",
+            "sub": {"name": "SubItem1"},
+            "multi": [{"name": "Multi1"}, {"name": "Multi2"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert isinstance(result, list)
+    assert len(result) == 2
+    for item in result:
+        assert item == {
+            "title": "Test Item",
+            "size": 100,
+            "description": "This is a test item",
+            "sub": {"name": "SubItem1"},
+            "multi": [{"name": "Multi1"}, {"name": "Multi2"}],
+        }
+
+
+def test_item_to_list_filter():
+    response = client.post(
+        "/item-filter",
+        json={
+            "title": "Filtered Item",
+            "size": 200,
+            "description": "Test filtering",
+            "sub": {"name": "SubFiltered"},
+            "multi": [],
+        },
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert isinstance(result, list)
+    assert len(result) == 2
+    for item in result:
+        assert item == {
+            "title": "Filtered Item",
+            "size": 200,
+            "description": "Test filtering",
+            "sub": {"name": "SubFiltered"},
+            "multi": [],
+        }
+        # Verify secret fields are filtered out
+        assert "secret_data" not in item
+        assert "internal_id" not in item
+        assert "internal_id" not in item["sub"]
+
+
+def test_list_to_item():
+    response = client.post(
+        "/item-list",
+        json=[
+            {"title": "First Item", "size": 50, "sub": {"name": "First Sub"}},
+            {"title": "Second Item", "size": 75, "sub": {"name": "Second Sub"}},
+        ],
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "First Item",
+        "size": 50,
+        "description": None,
+        "sub": {"name": "First Sub"},
+        "multi": [],
+    }
+
+
+def test_list_to_item_empty():
+    response = client.post(
+        "/item-list",
+        json=[],
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "",
+        "size": 0,
+        "description": None,
+        "sub": {"name": ""},
+        "multi": [],
+    }
+
+
+def test_list_to_item_filter():
+    response = client.post(
+        "/item-list-filter",
+        json=[
+            {
+                "title": "First Item",
+                "size": 100,
+                "sub": {"name": "First Sub"},
+                "multi": [{"name": "Multi1"}],
+            },
+            {"title": "Second Item", "size": 200, "sub": {"name": "Second Sub"}},
+        ],
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result == {
+        "title": "First Item",
+        "size": 100,
+        "description": None,
+        "sub": {"name": "First Sub"},
+        "multi": [{"name": "Multi1"}],
+    }
+    # Verify secret fields are filtered out
+    assert "secret_data" not in result
+    assert "internal_id" not in result
+
+
+def test_list_to_item_filter_no_data():
+    response = client.post("/item-list-filter", json=[])
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "",
+        "size": 0,
+        "description": None,
+        "sub": {"name": ""},
+        "multi": [],
+    }
+
+
+def test_list_to_list():
+    input_items = [
+        {"title": "Item 1", "size": 10, "sub": {"name": "Sub1"}},
+        {
+            "title": "Item 2",
+            "size": 20,
+            "description": "Second item",
+            "sub": {"name": "Sub2"},
+            "multi": [{"name": "M1"}, {"name": "M2"}],
+        },
+        {"title": "Item 3", "size": 30, "sub": {"name": "Sub3"}},
+    ]
+    response = client.post(
+        "/item-list-to-list",
+        json=input_items,
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert isinstance(result, list)
+    assert len(result) == 3
+    assert result[0] == {
+        "title": "Item 1",
+        "size": 10,
+        "description": None,
+        "sub": {"name": "Sub1"},
+        "multi": [],
+    }
+    assert result[1] == {
+        "title": "Item 2",
+        "size": 20,
+        "description": "Second item",
+        "sub": {"name": "Sub2"},
+        "multi": [{"name": "M1"}, {"name": "M2"}],
+    }
+    assert result[2] == {
+        "title": "Item 3",
+        "size": 30,
+        "description": None,
+        "sub": {"name": "Sub3"},
+        "multi": [],
+    }
+
+
+def test_list_to_list_filter():
+    response = client.post(
+        "/item-list-to-list-filter",
+        json=[{"title": "Item 1", "size": 100, "sub": {"name": "Sub1"}}],
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert isinstance(result, list)
+    assert len(result) == 2
+    for item in result:
+        assert item == {
+            "title": "Item 1",
+            "size": 100,
+            "description": None,
+            "sub": {"name": "Sub1"},
+            "multi": [],
+        }
+        # Verify secret fields are filtered out
+        assert "secret_data" not in item
+        assert "internal_id" not in item
+
+
+def test_list_to_list_filter_no_data():
+    response = client.post(
+        "/item-list-to-list-filter",
+        json=[],
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == []
+
+
+def test_list_validation_error():
+    response = client.post(
+        "/item-list",
+        json=[
+            {"title": "Valid Item", "size": 100, "sub": {"name": "Sub1"}},
+            {
+                "title": "Invalid Item"
+                # Missing required fields: size and sub
+            },
+        ],
+    )
+    assert response.status_code == 422, response.text
+    error_detail = response.json()["detail"]
+    assert len(error_detail) == 2
+    assert {
+        "loc": ["body", 1, "size"],
+        "msg": "field required",
+        "type": "value_error.missing",
+    } in error_detail
+    assert {
+        "loc": ["body", 1, "sub"],
+        "msg": "field required",
+        "type": "value_error.missing",
+    } in error_detail
+
+
+def test_list_nested_validation_error():
+    response = client.post(
+        "/item-list",
+        json=[
+            {"title": "Item with bad sub", "size": 100, "sub": {"wrong_field": "value"}}
+        ],
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", 0, "sub", "name"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
+
+
+def test_list_type_validation_error():
+    response = client.post(
+        "/item-list",
+        json=[{"title": "Item", "size": "not_a_number", "sub": {"name": "Sub"}}],
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", 0, "size"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                }
+            ]
+        }
+    )
+
+
+def test_invalid_list_structure():
+    response = client.post(
+        "/item-list",
+        json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body"],
+                    "msg": "value is not a valid list",
+                    "type": "type_error.list",
+                }
+            ]
+        }
+    )
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/item": {
+                    "post": {
+                        "summary": "Handle Item",
+                        "operationId": "handle_item_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle Item Item Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/item-filter": {
+                    "post": {
+                        "summary": "Handle Item Filter",
+                        "operationId": "handle_item_filter_item_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle Item Filter Item Filter Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/item-list": {
+                    "post": {
+                        "summary": "Handle Item List",
+                        "operationId": "handle_item_list_item_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/item-list-filter": {
+                    "post": {
+                        "summary": "Handle Item List Filter",
+                        "operationId": "handle_item_list_filter_item_list_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/item-list-to-list": {
+                    "post": {
+                        "summary": "Handle Item List To List",
+                        "operationId": "handle_item_list_to_list_item_list_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle Item List To List Item List To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/item-list-to-list-filter": {
+                    "post": {
+                        "summary": "Handle Item List To List Filter",
+                        "operationId": "handle_item_list_to_list_filter_item_list_to_list_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle Item List To List Filter Item List To List Filter Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+            },
+            "components": {
+                "schemas": {
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
+                        },
+                        "type": "object",
+                        "title": "HTTPValidationError",
+                    },
+                    "Item": {
+                        "properties": {
+                            "title": {"type": "string", "title": "Title"},
+                            "size": {"type": "integer", "title": "Size"},
+                            "description": {"type": "string", "title": "Description"},
+                            "sub": {"$ref": "#/components/schemas/SubItem"},
+                            "multi": {
+                                "items": {"$ref": "#/components/schemas/SubItem"},
+                                "type": "array",
+                                "title": "Multi",
+                                "default": [],
+                            },
+                        },
+                        "type": "object",
+                        "required": ["title", "size", "sub"],
+                        "title": "Item",
+                    },
+                    "SubItem": {
+                        "properties": {"name": {"type": "string", "title": "Name"}},
+                        "type": "object",
+                        "required": ["name"],
+                        "title": "SubItem",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
+                            },
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
+                        },
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                    },
+                }
+            },
+        }
+    )
diff --git a/tests/test_pydantic_v1_v2_mixed.py b/tests/test_pydantic_v1_v2_mixed.py
new file mode 100644 (file)
index 0000000..54d4088
--- /dev/null
@@ -0,0 +1,1499 @@
+import sys
+from typing import Any, List, Union
+
+from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+from fastapi import FastAPI
+from fastapi._compat.v1 import BaseModel
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from pydantic import BaseModel as NewBaseModel
+
+
+class SubItem(BaseModel):
+    name: str
+
+
+class Item(BaseModel):
+    title: str
+    size: int
+    description: Union[str, None] = None
+    sub: SubItem
+    multi: List[SubItem] = []
+
+
+class NewSubItem(NewBaseModel):
+    new_sub_name: str
+
+
+class NewItem(NewBaseModel):
+    new_title: str
+    new_size: int
+    new_description: Union[str, None] = None
+    new_sub: NewSubItem
+    new_multi: List[NewSubItem] = []
+
+
+app = FastAPI()
+
+
+@app.post("/v1-to-v2/item")
+def handle_v1_item_to_v2(data: Item) -> NewItem:
+    return NewItem(
+        new_title=data.title,
+        new_size=data.size,
+        new_description=data.description,
+        new_sub=NewSubItem(new_sub_name=data.sub.name),
+        new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
+    )
+
+
+@app.post("/v1-to-v2/item-filter", response_model=NewItem)
+def handle_v1_item_to_v2_filter(data: Item) -> Any:
+    result = {
+        "new_title": data.title,
+        "new_size": data.size,
+        "new_description": data.description,
+        "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"},
+        "new_multi": [
+            {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi
+        ],
+        "secret": "hidden_v1_to_v2",
+    }
+    return result
+
+
+@app.post("/v2-to-v1/item")
+def handle_v2_item_to_v1(data: NewItem) -> Item:
+    return Item(
+        title=data.new_title,
+        size=data.new_size,
+        description=data.new_description,
+        sub=SubItem(name=data.new_sub.new_sub_name),
+        multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
+    )
+
+
+@app.post("/v2-to-v1/item-filter", response_model=Item)
+def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
+    result = {
+        "title": data.new_title,
+        "size": data.new_size,
+        "description": data.new_description,
+        "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
+        "multi": [
+            {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi
+        ],
+        "secret": "hidden_v2_to_v1",
+    }
+    return result
+
+
+@app.post("/v1-to-v2/item-to-list")
+def handle_v1_item_to_v2_list(data: Item) -> List[NewItem]:
+    converted = NewItem(
+        new_title=data.title,
+        new_size=data.size,
+        new_description=data.description,
+        new_sub=NewSubItem(new_sub_name=data.sub.name),
+        new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
+    )
+    return [converted, converted]
+
+
+@app.post("/v1-to-v2/list-to-list")
+def handle_v1_list_to_v2_list(data: List[Item]) -> List[NewItem]:
+    result = []
+    for item in data:
+        result.append(
+            NewItem(
+                new_title=item.title,
+                new_size=item.size,
+                new_description=item.description,
+                new_sub=NewSubItem(new_sub_name=item.sub.name),
+                new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
+            )
+        )
+    return result
+
+
+@app.post("/v1-to-v2/list-to-list-filter", response_model=List[NewItem])
+def handle_v1_list_to_v2_list_filter(data: List[Item]) -> Any:
+    result = []
+    for item in data:
+        converted = {
+            "new_title": item.title,
+            "new_size": item.size,
+            "new_description": item.description,
+            "new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"},
+            "new_multi": [
+                {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
+                for s in item.multi
+            ],
+            "secret": "hidden_v2_to_v1",
+        }
+        result.append(converted)
+    return result
+
+
+@app.post("/v1-to-v2/list-to-item")
+def handle_v1_list_to_v2_item(data: List[Item]) -> NewItem:
+    if data:
+        item = data[0]
+        return NewItem(
+            new_title=item.title,
+            new_size=item.size,
+            new_description=item.description,
+            new_sub=NewSubItem(new_sub_name=item.sub.name),
+            new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
+        )
+    return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name=""))
+
+
+@app.post("/v2-to-v1/item-to-list")
+def handle_v2_item_to_v1_list(data: NewItem) -> List[Item]:
+    converted = Item(
+        title=data.new_title,
+        size=data.new_size,
+        description=data.new_description,
+        sub=SubItem(name=data.new_sub.new_sub_name),
+        multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
+    )
+    return [converted, converted]
+
+
+@app.post("/v2-to-v1/list-to-list")
+def handle_v2_list_to_v1_list(data: List[NewItem]) -> List[Item]:
+    result = []
+    for item in data:
+        result.append(
+            Item(
+                title=item.new_title,
+                size=item.new_size,
+                description=item.new_description,
+                sub=SubItem(name=item.new_sub.new_sub_name),
+                multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
+            )
+        )
+    return result
+
+
+@app.post("/v2-to-v1/list-to-list-filter", response_model=List[Item])
+def handle_v2_list_to_v1_list_filter(data: List[NewItem]) -> Any:
+    result = []
+    for item in data:
+        converted = {
+            "title": item.new_title,
+            "size": item.new_size,
+            "description": item.new_description,
+            "sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
+            "multi": [
+                {"name": s.new_sub_name, "sub_secret": "sub_hidden"}
+                for s in item.new_multi
+            ],
+            "secret": "hidden_v2_to_v1",
+        }
+        result.append(converted)
+    return result
+
+
+@app.post("/v2-to-v1/list-to-item")
+def handle_v2_list_to_v1_item(data: List[NewItem]) -> Item:
+    if data:
+        item = data[0]
+        return Item(
+            title=item.new_title,
+            size=item.new_size,
+            description=item.new_description,
+            sub=SubItem(name=item.new_sub.new_sub_name),
+            multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
+        )
+    return Item(title="", size=0, sub=SubItem(name=""))
+
+
+client = TestClient(app)
+
+
+def test_v1_to_v2_item():
+    response = client.post(
+        "/v1-to-v2/item",
+        json={
+            "title": "Old Item",
+            "size": 100,
+            "description": "V1 description",
+            "sub": {"name": "V1 Sub"},
+            "multi": [{"name": "M1"}, {"name": "M2"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "new_title": "Old Item",
+        "new_size": 100,
+        "new_description": "V1 description",
+        "new_sub": {"new_sub_name": "V1 Sub"},
+        "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
+    }
+
+
+def test_v1_to_v2_item_minimal():
+    response = client.post(
+        "/v1-to-v2/item",
+        json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "new_title": "Minimal",
+        "new_size": 50,
+        "new_description": None,
+        "new_sub": {"new_sub_name": "MinSub"},
+        "new_multi": [],
+    }
+
+
+def test_v1_to_v2_item_filter():
+    response = client.post(
+        "/v1-to-v2/item-filter",
+        json={
+            "title": "Filtered Item",
+            "size": 50,
+            "sub": {"name": "Sub"},
+            "multi": [{"name": "Multi1"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result == snapshot(
+        {
+            "new_title": "Filtered Item",
+            "new_size": 50,
+            "new_description": None,
+            "new_sub": {"new_sub_name": "Sub"},
+            "new_multi": [{"new_sub_name": "Multi1"}],
+        }
+    )
+    # Verify secret fields are filtered out
+    assert "secret" not in result
+    assert "new_sub_secret" not in result["new_sub"]
+    assert "new_sub_secret" not in result["new_multi"][0]
+
+
+def test_v2_to_v1_item():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "New Item",
+            "new_size": 200,
+            "new_description": "V2 description",
+            "new_sub": {"new_sub_name": "V2 Sub"},
+            "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "New Item",
+        "size": 200,
+        "description": "V2 description",
+        "sub": {"name": "V2 Sub"},
+        "multi": [{"name": "N1"}, {"name": "N2"}],
+    }
+
+
+def test_v2_to_v1_item_minimal():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "MinimalNew",
+            "new_size": 75,
+            "new_sub": {"new_sub_name": "MinNewSub"},
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "MinimalNew",
+        "size": 75,
+        "description": None,
+        "sub": {"name": "MinNewSub"},
+        "multi": [],
+    }
+
+
+def test_v2_to_v1_item_filter():
+    response = client.post(
+        "/v2-to-v1/item-filter",
+        json={
+            "new_title": "Filtered New",
+            "new_size": 75,
+            "new_sub": {"new_sub_name": "NewSub"},
+            "new_multi": [],
+        },
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result == snapshot(
+        {
+            "title": "Filtered New",
+            "size": 75,
+            "description": None,
+            "sub": {"name": "NewSub"},
+            "multi": [],
+        }
+    )
+    # Verify secret fields are filtered out
+    assert "secret" not in result
+    assert "sub_secret" not in result["sub"]
+
+
+def test_v1_item_to_v2_list():
+    response = client.post(
+        "/v1-to-v2/item-to-list",
+        json={
+            "title": "Single to List",
+            "size": 150,
+            "description": "Convert to list",
+            "sub": {"name": "Sub1"},
+            "multi": [],
+        },
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result == [
+        {
+            "new_title": "Single to List",
+            "new_size": 150,
+            "new_description": "Convert to list",
+            "new_sub": {"new_sub_name": "Sub1"},
+            "new_multi": [],
+        },
+        {
+            "new_title": "Single to List",
+            "new_size": 150,
+            "new_description": "Convert to list",
+            "new_sub": {"new_sub_name": "Sub1"},
+            "new_multi": [],
+        },
+    ]
+
+
+def test_v1_list_to_v2_list():
+    response = client.post(
+        "/v1-to-v2/list-to-list",
+        json=[
+            {"title": "Item1", "size": 10, "sub": {"name": "Sub1"}},
+            {
+                "title": "Item2",
+                "size": 20,
+                "description": "Second item",
+                "sub": {"name": "Sub2"},
+                "multi": [{"name": "M1"}, {"name": "M2"}],
+            },
+            {"title": "Item3", "size": 30, "sub": {"name": "Sub3"}},
+        ],
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == [
+        {
+            "new_title": "Item1",
+            "new_size": 10,
+            "new_description": None,
+            "new_sub": {"new_sub_name": "Sub1"},
+            "new_multi": [],
+        },
+        {
+            "new_title": "Item2",
+            "new_size": 20,
+            "new_description": "Second item",
+            "new_sub": {"new_sub_name": "Sub2"},
+            "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
+        },
+        {
+            "new_title": "Item3",
+            "new_size": 30,
+            "new_description": None,
+            "new_sub": {"new_sub_name": "Sub3"},
+            "new_multi": [],
+        },
+    ]
+
+
+def test_v1_list_to_v2_list_filter():
+    response = client.post(
+        "/v1-to-v2/list-to-list-filter",
+        json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}],
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result == snapshot(
+        [
+            {
+                "new_title": "FilterMe",
+                "new_size": 30,
+                "new_description": None,
+                "new_sub": {"new_sub_name": "SubF"},
+                "new_multi": [],
+            }
+        ]
+    )
+    # Verify secret fields are filtered out
+    assert "secret" not in result[0]
+    assert "new_sub_secret" not in result[0]["new_sub"]
+
+
+def test_v1_list_to_v2_item():
+    response = client.post(
+        "/v1-to-v2/list-to-item",
+        json=[
+            {"title": "First", "size": 100, "sub": {"name": "FirstSub"}},
+            {"title": "Second", "size": 200, "sub": {"name": "SecondSub"}},
+        ],
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "new_title": "First",
+        "new_size": 100,
+        "new_description": None,
+        "new_sub": {"new_sub_name": "FirstSub"},
+        "new_multi": [],
+    }
+
+
+def test_v1_list_to_v2_item_empty():
+    response = client.post("/v1-to-v2/list-to-item", json=[])
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "new_title": "",
+        "new_size": 0,
+        "new_description": None,
+        "new_sub": {"new_sub_name": ""},
+        "new_multi": [],
+    }
+
+
+def test_v2_item_to_v1_list():
+    response = client.post(
+        "/v2-to-v1/item-to-list",
+        json={
+            "new_title": "Single New",
+            "new_size": 250,
+            "new_description": "New to list",
+            "new_sub": {"new_sub_name": "NewSub"},
+            "new_multi": [],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == [
+        {
+            "title": "Single New",
+            "size": 250,
+            "description": "New to list",
+            "sub": {"name": "NewSub"},
+            "multi": [],
+        },
+        {
+            "title": "Single New",
+            "size": 250,
+            "description": "New to list",
+            "sub": {"name": "NewSub"},
+            "multi": [],
+        },
+    ]
+
+
+def test_v2_list_to_v1_list():
+    response = client.post(
+        "/v2-to-v1/list-to-list",
+        json=[
+            {"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}},
+            {
+                "new_title": "New2",
+                "new_size": 25,
+                "new_description": "Second new",
+                "new_sub": {"new_sub_name": "NS2"},
+                "new_multi": [{"new_sub_name": "NM1"}],
+            },
+        ],
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == [
+        {
+            "title": "New1",
+            "size": 15,
+            "description": None,
+            "sub": {"name": "NS1"},
+            "multi": [],
+        },
+        {
+            "title": "New2",
+            "size": 25,
+            "description": "Second new",
+            "sub": {"name": "NS2"},
+            "multi": [{"name": "NM1"}],
+        },
+    ]
+
+
+def test_v2_list_to_v1_list_filter():
+    response = client.post(
+        "/v2-to-v1/list-to-list-filter",
+        json=[
+            {
+                "new_title": "FilterNew",
+                "new_size": 35,
+                "new_sub": {"new_sub_name": "NSF"},
+            }
+        ],
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result == snapshot(
+        [
+            {
+                "title": "FilterNew",
+                "size": 35,
+                "description": None,
+                "sub": {"name": "NSF"},
+                "multi": [],
+            }
+        ]
+    )
+    # Verify secret fields are filtered out
+    assert "secret" not in result[0]
+    assert "sub_secret" not in result[0]["sub"]
+
+
+def test_v2_list_to_v1_item():
+    response = client.post(
+        "/v2-to-v1/list-to-item",
+        json=[
+            {
+                "new_title": "FirstNew",
+                "new_size": 300,
+                "new_sub": {"new_sub_name": "FNS"},
+            },
+            {
+                "new_title": "SecondNew",
+                "new_size": 400,
+                "new_sub": {"new_sub_name": "SNS"},
+            },
+        ],
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "FirstNew",
+        "size": 300,
+        "description": None,
+        "sub": {"name": "FNS"},
+        "multi": [],
+    }
+
+
+def test_v2_list_to_v1_item_empty():
+    response = client.post("/v2-to-v1/list-to-item", json=[])
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "",
+        "size": 0,
+        "description": None,
+        "sub": {"name": ""},
+        "multi": [],
+    }
+
+
+def test_v1_to_v2_validation_error():
+    response = client.post("/v1-to-v2/item", json={"title": "Missing fields"})
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "size"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "sub"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
+
+
+def test_v1_to_v2_nested_validation_error():
+    response = client.post(
+        "/v1-to-v2/item",
+        json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "sub", "name"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
+
+
+def test_v1_to_v2_type_validation_error():
+    response = client.post(
+        "/v1-to-v2/item",
+        json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "size"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                }
+            ]
+        }
+    )
+
+
+def test_v2_to_v1_validation_error():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={"new_title": "Missing fields"},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": pydantic_snapshot(
+                v2=snapshot(
+                    [
+                        {
+                            "type": "missing",
+                            "loc": ["body", "new_size"],
+                            "msg": "Field required",
+                            "input": {"new_title": "Missing fields"},
+                        },
+                        {
+                            "type": "missing",
+                            "loc": ["body", "new_sub"],
+                            "msg": "Field required",
+                            "input": {"new_title": "Missing fields"},
+                        },
+                    ]
+                ),
+                v1=snapshot(
+                    [
+                        {
+                            "loc": ["body", "new_size"],
+                            "msg": "field required",
+                            "type": "value_error.missing",
+                        },
+                        {
+                            "loc": ["body", "new_sub"],
+                            "msg": "field required",
+                            "type": "value_error.missing",
+                        },
+                    ]
+                ),
+            )
+        }
+    )
+
+
+def test_v2_to_v1_nested_validation_error():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "Bad sub",
+            "new_size": 200,
+            "new_sub": {"wrong_field": "value"},
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                pydantic_snapshot(
+                    v2=snapshot(
+                        {
+                            "type": "missing",
+                            "loc": ["body", "new_sub", "new_sub_name"],
+                            "msg": "Field required",
+                            "input": {"wrong_field": "value"},
+                        }
+                    ),
+                    v1=snapshot(
+                        {
+                            "loc": ["body", "new_sub", "new_sub_name"],
+                            "msg": "field required",
+                            "type": "value_error.missing",
+                        }
+                    ),
+                )
+            ]
+        }
+    )
+
+
+def test_v1_list_validation_error():
+    response = client.post(
+        "/v1-to-v2/list-to-list",
+        json=[
+            {"title": "Valid", "size": 10, "sub": {"name": "S"}},
+            {"title": "Invalid"},
+        ],
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", 1, "size"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", 1, "sub"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
+
+
+def test_v2_list_validation_error():
+    response = client.post(
+        "/v2-to-v1/list-to-list",
+        json=[
+            {"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}},
+            {"new_title": "Invalid"},
+        ],
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": pydantic_snapshot(
+                v2=snapshot(
+                    [
+                        {
+                            "type": "missing",
+                            "loc": ["body", 1, "new_size"],
+                            "msg": "Field required",
+                            "input": {"new_title": "Invalid"},
+                        },
+                        {
+                            "type": "missing",
+                            "loc": ["body", 1, "new_sub"],
+                            "msg": "Field required",
+                            "input": {"new_title": "Invalid"},
+                        },
+                    ]
+                ),
+                v1=snapshot(
+                    [
+                        {
+                            "loc": ["body", 1, "new_size"],
+                            "msg": "field required",
+                            "type": "value_error.missing",
+                        },
+                        {
+                            "loc": ["body", 1, "new_sub"],
+                            "msg": "field required",
+                            "type": "value_error.missing",
+                        },
+                    ]
+                ),
+            )
+        }
+    )
+
+
+def test_invalid_list_structure_v1():
+    response = client.post(
+        "/v1-to-v2/list-to-list",
+        json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body"],
+                    "msg": "value is not a valid list",
+                    "type": "type_error.list",
+                }
+            ]
+        }
+    )
+
+
+def test_invalid_list_structure_v2():
+    response = client.post(
+        "/v2-to-v1/list-to-list",
+        json={
+            "new_title": "Not a list",
+            "new_size": 100,
+            "new_sub": {"new_sub_name": "Sub"},
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": pydantic_snapshot(
+                v2=snapshot(
+                    [
+                        {
+                            "type": "list_type",
+                            "loc": ["body"],
+                            "msg": "Input should be a valid list",
+                            "input": {
+                                "new_title": "Not a list",
+                                "new_size": 100,
+                                "new_sub": {"new_sub_name": "Sub"},
+                            },
+                        }
+                    ]
+                ),
+                v1=snapshot(
+                    [
+                        {
+                            "loc": ["body"],
+                            "msg": "value is not a valid list",
+                            "type": "type_error.list",
+                        }
+                    ]
+                ),
+            )
+        }
+    )
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/v1-to-v2/item": {
+                    "post": {
+                        "summary": "Handle V1 Item To V2",
+                        "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/NewItem"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/item-filter": {
+                    "post": {
+                        "summary": "Handle V1 Item To V2 Filter",
+                        "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/NewItem"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/item": {
+                    "post": {
+                        "summary": "Handle V2 Item To V1",
+                        "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/NewItem"}
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/item-filter": {
+                    "post": {
+                        "summary": "Handle V2 Item To V1 Filter",
+                        "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/NewItem"}
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/item-to-list": {
+                    "post": {
+                        "summary": "Handle V1 Item To V2 List",
+                        "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/NewItem"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/list-to-list": {
+                    "post": {
+                        "summary": "Handle V1 List To V2 List",
+                        "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/NewItem"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/list-to-list-filter": {
+                    "post": {
+                        "summary": "Handle V1 List To V2 List Filter",
+                        "operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_list_to_list_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/NewItem"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/list-to-item": {
+                    "post": {
+                        "summary": "Handle V1 List To V2 Item",
+                        "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/NewItem"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/item-to-list": {
+                    "post": {
+                        "summary": "Handle V2 Item To V1 List",
+                        "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/NewItem"}
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/list-to-list": {
+                    "post": {
+                        "summary": "Handle V2 List To V1 List",
+                        "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/NewItem"
+                                        },
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/list-to-list-filter": {
+                    "post": {
+                        "summary": "Handle V2 List To V1 List Filter",
+                        "operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/NewItem"
+                                        },
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V2 List To V1 List Filter V2 To V1 List To List Filter Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/list-to-item": {
+                    "post": {
+                        "summary": "Handle V2 List To V1 Item",
+                        "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/NewItem"
+                                        },
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+            },
+            "components": {
+                "schemas": {
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
+                        },
+                        "type": "object",
+                        "title": "HTTPValidationError",
+                    },
+                    "Item": {
+                        "properties": {
+                            "title": {"type": "string", "title": "Title"},
+                            "size": {"type": "integer", "title": "Size"},
+                            "description": {"type": "string", "title": "Description"},
+                            "sub": {"$ref": "#/components/schemas/SubItem"},
+                            "multi": {
+                                "items": {"$ref": "#/components/schemas/SubItem"},
+                                "type": "array",
+                                "title": "Multi",
+                                "default": [],
+                            },
+                        },
+                        "type": "object",
+                        "required": ["title", "size", "sub"],
+                        "title": "Item",
+                    },
+                    "NewItem": {
+                        "properties": {
+                            "new_title": {"type": "string", "title": "New Title"},
+                            "new_size": {"type": "integer", "title": "New Size"},
+                            "new_description": pydantic_snapshot(
+                                v2=snapshot(
+                                    {
+                                        "anyOf": [{"type": "string"}, {"type": "null"}],
+                                        "title": "New Description",
+                                    }
+                                ),
+                                v1=snapshot(
+                                    {"type": "string", "title": "New Description"}
+                                ),
+                            ),
+                            "new_sub": {"$ref": "#/components/schemas/NewSubItem"},
+                            "new_multi": {
+                                "items": {"$ref": "#/components/schemas/NewSubItem"},
+                                "type": "array",
+                                "title": "New Multi",
+                                "default": [],
+                            },
+                        },
+                        "type": "object",
+                        "required": ["new_title", "new_size", "new_sub"],
+                        "title": "NewItem",
+                    },
+                    "NewSubItem": {
+                        "properties": {
+                            "new_sub_name": {"type": "string", "title": "New Sub Name"}
+                        },
+                        "type": "object",
+                        "required": ["new_sub_name"],
+                        "title": "NewSubItem",
+                    },
+                    "SubItem": {
+                        "properties": {"name": {"type": "string", "title": "Name"}},
+                        "type": "object",
+                        "required": ["name"],
+                        "title": "SubItem",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
+                            },
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
+                        },
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                    },
+                }
+            },
+        }
+    )
diff --git a/tests/test_pydantic_v1_v2_multifile/__init__.py b/tests/test_pydantic_v1_v2_multifile/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_pydantic_v1_v2_multifile/main.py b/tests/test_pydantic_v1_v2_multifile/main.py
new file mode 100644 (file)
index 0000000..8985cb7
--- /dev/null
@@ -0,0 +1,142 @@
+from typing import List
+
+from fastapi import FastAPI
+
+from . import modelsv1, modelsv2, modelsv2b
+
+app = FastAPI()
+
+
+@app.post("/v1-to-v2/item")
+def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item:
+    return modelsv2.Item(
+        new_title=data.title,
+        new_size=data.size,
+        new_description=data.description,
+        new_sub=modelsv2.SubItem(new_sub_name=data.sub.name),
+        new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
+    )
+
+
+@app.post("/v2-to-v1/item")
+def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item:
+    return modelsv1.Item(
+        title=data.new_title,
+        size=data.new_size,
+        description=data.new_description,
+        sub=modelsv1.SubItem(name=data.new_sub.new_sub_name),
+        multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
+    )
+
+
+@app.post("/v1-to-v2/item-to-list")
+def handle_v1_item_to_v2_list(data: modelsv1.Item) -> List[modelsv2.Item]:
+    converted = modelsv2.Item(
+        new_title=data.title,
+        new_size=data.size,
+        new_description=data.description,
+        new_sub=modelsv2.SubItem(new_sub_name=data.sub.name),
+        new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
+    )
+    return [converted, converted]
+
+
+@app.post("/v1-to-v2/list-to-list")
+def handle_v1_list_to_v2_list(data: List[modelsv1.Item]) -> List[modelsv2.Item]:
+    result = []
+    for item in data:
+        result.append(
+            modelsv2.Item(
+                new_title=item.title,
+                new_size=item.size,
+                new_description=item.description,
+                new_sub=modelsv2.SubItem(new_sub_name=item.sub.name),
+                new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi],
+            )
+        )
+    return result
+
+
+@app.post("/v1-to-v2/list-to-item")
+def handle_v1_list_to_v2_item(data: List[modelsv1.Item]) -> modelsv2.Item:
+    if data:
+        item = data[0]
+        return modelsv2.Item(
+            new_title=item.title,
+            new_size=item.size,
+            new_description=item.description,
+            new_sub=modelsv2.SubItem(new_sub_name=item.sub.name),
+            new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi],
+        )
+    return modelsv2.Item(
+        new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="")
+    )
+
+
+@app.post("/v2-to-v1/item-to-list")
+def handle_v2_item_to_v1_list(data: modelsv2.Item) -> List[modelsv1.Item]:
+    converted = modelsv1.Item(
+        title=data.new_title,
+        size=data.new_size,
+        description=data.new_description,
+        sub=modelsv1.SubItem(name=data.new_sub.new_sub_name),
+        multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
+    )
+    return [converted, converted]
+
+
+@app.post("/v2-to-v1/list-to-list")
+def handle_v2_list_to_v1_list(data: List[modelsv2.Item]) -> List[modelsv1.Item]:
+    result = []
+    for item in data:
+        result.append(
+            modelsv1.Item(
+                title=item.new_title,
+                size=item.new_size,
+                description=item.new_description,
+                sub=modelsv1.SubItem(name=item.new_sub.new_sub_name),
+                multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi],
+            )
+        )
+    return result
+
+
+@app.post("/v2-to-v1/list-to-item")
+def handle_v2_list_to_v1_item(data: List[modelsv2.Item]) -> modelsv1.Item:
+    if data:
+        item = data[0]
+        return modelsv1.Item(
+            title=item.new_title,
+            size=item.new_size,
+            description=item.new_description,
+            sub=modelsv1.SubItem(name=item.new_sub.new_sub_name),
+            multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi],
+        )
+    return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name=""))
+
+
+@app.post("/v2-to-v1/same-name")
+def handle_v2_same_name_to_v1(
+    item1: modelsv2.Item, item2: modelsv2b.Item
+) -> modelsv1.Item:
+    return modelsv1.Item(
+        title=item1.new_title,
+        size=item2.dup_size,
+        description=item1.new_description,
+        sub=modelsv1.SubItem(name=item1.new_sub.new_sub_name),
+        multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi],
+    )
+
+
+@app.post("/v2-to-v1/list-of-items-to-list-of-items")
+def handle_v2_items_in_list_to_v1_item_in_list(
+    data1: List[modelsv2.ItemInList], data2: List[modelsv2b.ItemInList]
+) -> List[modelsv1.ItemInList]:
+    result = []
+    item1 = data1[0]
+    item2 = data2[0]
+    result = [
+        modelsv1.ItemInList(name1=item1.name2),
+        modelsv1.ItemInList(name1=item2.dup_name2),
+    ]
+    return result
diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv1.py b/tests/test_pydantic_v1_v2_multifile/modelsv1.py
new file mode 100644 (file)
index 0000000..889291a
--- /dev/null
@@ -0,0 +1,19 @@
+from typing import List, Union
+
+from fastapi._compat.v1 import BaseModel
+
+
+class SubItem(BaseModel):
+    name: str
+
+
+class Item(BaseModel):
+    title: str
+    size: int
+    description: Union[str, None] = None
+    sub: SubItem
+    multi: List[SubItem] = []
+
+
+class ItemInList(BaseModel):
+    name1: str
diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2.py b/tests/test_pydantic_v1_v2_multifile/modelsv2.py
new file mode 100644 (file)
index 0000000..2c8c6ea
--- /dev/null
@@ -0,0 +1,19 @@
+from typing import List, Union
+
+from pydantic import BaseModel
+
+
+class SubItem(BaseModel):
+    new_sub_name: str
+
+
+class Item(BaseModel):
+    new_title: str
+    new_size: int
+    new_description: Union[str, None] = None
+    new_sub: SubItem
+    new_multi: List[SubItem] = []
+
+
+class ItemInList(BaseModel):
+    name2: str
diff --git a/tests/test_pydantic_v1_v2_multifile/modelsv2b.py b/tests/test_pydantic_v1_v2_multifile/modelsv2b.py
new file mode 100644 (file)
index 0000000..dc0c06c
--- /dev/null
@@ -0,0 +1,19 @@
+from typing import List, Union
+
+from pydantic import BaseModel
+
+
+class SubItem(BaseModel):
+    dup_sub_name: str
+
+
+class Item(BaseModel):
+    dup_title: str
+    dup_size: int
+    dup_description: Union[str, None] = None
+    dup_sub: SubItem
+    dup_multi: List[SubItem] = []
+
+
+class ItemInList(BaseModel):
+    dup_name2: str
diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py
new file mode 100644 (file)
index 0000000..4472bd7
--- /dev/null
@@ -0,0 +1,1237 @@
+import sys
+
+from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+
+from .main import app
+
+client = TestClient(app)
+
+
+def test_v1_to_v2_item():
+    response = client.post(
+        "/v1-to-v2/item",
+        json={"title": "Test", "size": 10, "sub": {"name": "SubTest"}},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "new_title": "Test",
+        "new_size": 10,
+        "new_description": None,
+        "new_sub": {"new_sub_name": "SubTest"},
+        "new_multi": [],
+    }
+
+
+def test_v2_to_v1_item():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "NewTest",
+            "new_size": 20,
+            "new_sub": {"new_sub_name": "NewSubTest"},
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "title": "NewTest",
+        "size": 20,
+        "description": None,
+        "sub": {"name": "NewSubTest"},
+        "multi": [],
+    }
+
+
+def test_v1_to_v2_item_to_list():
+    response = client.post(
+        "/v1-to-v2/item-to-list",
+        json={"title": "ListTest", "size": 30, "sub": {"name": "SubListTest"}},
+    )
+    assert response.status_code == 200
+    assert response.json() == [
+        {
+            "new_title": "ListTest",
+            "new_size": 30,
+            "new_description": None,
+            "new_sub": {"new_sub_name": "SubListTest"},
+            "new_multi": [],
+        },
+        {
+            "new_title": "ListTest",
+            "new_size": 30,
+            "new_description": None,
+            "new_sub": {"new_sub_name": "SubListTest"},
+            "new_multi": [],
+        },
+    ]
+
+
+def test_v1_to_v2_list_to_list():
+    response = client.post(
+        "/v1-to-v2/list-to-list",
+        json=[
+            {"title": "Item1", "size": 40, "sub": {"name": "Sub1"}},
+            {"title": "Item2", "size": 50, "sub": {"name": "Sub2"}},
+        ],
+    )
+    assert response.status_code == 200
+    assert response.json() == [
+        {
+            "new_title": "Item1",
+            "new_size": 40,
+            "new_description": None,
+            "new_sub": {"new_sub_name": "Sub1"},
+            "new_multi": [],
+        },
+        {
+            "new_title": "Item2",
+            "new_size": 50,
+            "new_description": None,
+            "new_sub": {"new_sub_name": "Sub2"},
+            "new_multi": [],
+        },
+    ]
+
+
+def test_v1_to_v2_list_to_item():
+    response = client.post(
+        "/v1-to-v2/list-to-item",
+        json=[
+            {"title": "FirstItem", "size": 60, "sub": {"name": "FirstSub"}},
+            {"title": "SecondItem", "size": 70, "sub": {"name": "SecondSub"}},
+        ],
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "new_title": "FirstItem",
+        "new_size": 60,
+        "new_description": None,
+        "new_sub": {"new_sub_name": "FirstSub"},
+        "new_multi": [],
+    }
+
+
+def test_v2_to_v1_item_to_list():
+    response = client.post(
+        "/v2-to-v1/item-to-list",
+        json={
+            "new_title": "ListNew",
+            "new_size": 80,
+            "new_sub": {"new_sub_name": "SubListNew"},
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == [
+        {
+            "title": "ListNew",
+            "size": 80,
+            "description": None,
+            "sub": {"name": "SubListNew"},
+            "multi": [],
+        },
+        {
+            "title": "ListNew",
+            "size": 80,
+            "description": None,
+            "sub": {"name": "SubListNew"},
+            "multi": [],
+        },
+    ]
+
+
+def test_v2_to_v1_list_to_list():
+    response = client.post(
+        "/v2-to-v1/list-to-list",
+        json=[
+            {
+                "new_title": "New1",
+                "new_size": 90,
+                "new_sub": {"new_sub_name": "NewSub1"},
+            },
+            {
+                "new_title": "New2",
+                "new_size": 100,
+                "new_sub": {"new_sub_name": "NewSub2"},
+            },
+        ],
+    )
+    assert response.status_code == 200
+    assert response.json() == [
+        {
+            "title": "New1",
+            "size": 90,
+            "description": None,
+            "sub": {"name": "NewSub1"},
+            "multi": [],
+        },
+        {
+            "title": "New2",
+            "size": 100,
+            "description": None,
+            "sub": {"name": "NewSub2"},
+            "multi": [],
+        },
+    ]
+
+
+def test_v2_to_v1_list_to_item():
+    response = client.post(
+        "/v2-to-v1/list-to-item",
+        json=[
+            {
+                "new_title": "FirstNew",
+                "new_size": 110,
+                "new_sub": {"new_sub_name": "FirstNewSub"},
+            },
+            {
+                "new_title": "SecondNew",
+                "new_size": 120,
+                "new_sub": {"new_sub_name": "SecondNewSub"},
+            },
+        ],
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "title": "FirstNew",
+        "size": 110,
+        "description": None,
+        "sub": {"name": "FirstNewSub"},
+        "multi": [],
+    }
+
+
+def test_v1_to_v2_list_to_item_empty():
+    response = client.post("/v1-to-v2/list-to-item", json=[])
+    assert response.status_code == 200
+    assert response.json() == {
+        "new_title": "",
+        "new_size": 0,
+        "new_description": None,
+        "new_sub": {"new_sub_name": ""},
+        "new_multi": [],
+    }
+
+
+def test_v2_to_v1_list_to_item_empty():
+    response = client.post("/v2-to-v1/list-to-item", json=[])
+    assert response.status_code == 200
+    assert response.json() == {
+        "title": "",
+        "size": 0,
+        "description": None,
+        "sub": {"name": ""},
+        "multi": [],
+    }
+
+
+def test_v2_same_name_to_v1():
+    response = client.post(
+        "/v2-to-v1/same-name",
+        json={
+            "item1": {
+                "new_title": "Title1",
+                "new_size": 100,
+                "new_description": "Description1",
+                "new_sub": {"new_sub_name": "Sub1"},
+                "new_multi": [{"new_sub_name": "Multi1"}],
+            },
+            "item2": {
+                "dup_title": "Title2",
+                "dup_size": 200,
+                "dup_description": "Description2",
+                "dup_sub": {"dup_sub_name": "Sub2"},
+                "dup_multi": [
+                    {"dup_sub_name": "Multi2a"},
+                    {"dup_sub_name": "Multi2b"},
+                ],
+            },
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "title": "Title1",
+        "size": 200,
+        "description": "Description1",
+        "sub": {"name": "Sub1"},
+        "multi": [{"name": "Multi2a"}, {"name": "Multi2b"}],
+    }
+
+
+def test_v2_items_in_list_to_v1_item_in_list():
+    response = client.post(
+        "/v2-to-v1/list-of-items-to-list-of-items",
+        json={
+            "data1": [{"name2": "Item1"}, {"name2": "Item2"}],
+            "data2": [{"dup_name2": "Item3"}, {"dup_name2": "Item4"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == [
+        {"name1": "Item1"},
+        {"name1": "Item3"},
+    ]
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/v1-to-v2/item": {
+                    "post": {
+                        "summary": "Handle V1 Item To V2",
+                        "operationId": "handle_v1_item_to_v2_v1_to_v2_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                            }
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/item": {
+                    "post": {
+                        "summary": "Handle V2 Item To V1",
+                        "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                            }
+                                        ),
+                                    ),
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/item-to-list": {
+                    "post": {
+                        "summary": "Handle V1 Item To V2 List",
+                        "operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                            }
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/list-to-list": {
+                    "post": {
+                        "summary": "Handle V1 List To V2 List",
+                        "operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                        },
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V1 List To V2 List V1 To V2 List To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/list-to-item": {
+                    "post": {
+                        "summary": "Handle V1 List To V2 Item",
+                        "operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                        },
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/item-to-list": {
+                    "post": {
+                        "summary": "Handle V2 Item To V1 List",
+                        "operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                            }
+                                        ),
+                                    ),
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/list-to-list": {
+                    "post": {
+                        "summary": "Handle V2 List To V1 List",
+                        "operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": pydantic_snapshot(
+                                            v2=snapshot(
+                                                {
+                                                    "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
+                                                }
+                                            ),
+                                            v1=snapshot(
+                                                {
+                                                    "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                                }
+                                            ),
+                                        ),
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V2 List To V1 List V2 To V1 List To List Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/list-to-item": {
+                    "post": {
+                        "summary": "Handle V2 List To V1 Item",
+                        "operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": pydantic_snapshot(
+                                            v2=snapshot(
+                                                {
+                                                    "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
+                                                }
+                                            ),
+                                            v1=snapshot(
+                                                {
+                                                    "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                                }
+                                            ),
+                                        ),
+                                        "type": "array",
+                                        "title": "Data",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/same-name": {
+                    "post": {
+                        "summary": "Handle V2 Same Name To V1",
+                        "operationId": "handle_v2_same_name_to_v1_v2_to_v1_same_name_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post"
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/list-of-items-to-list-of-items": {
+                    "post": {
+                        "summary": "Handle V2 Items In List To V1 Item In List",
+                        "operationId": "handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post"
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "items": {
+                                                "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList"
+                                            },
+                                            "type": "array",
+                                            "title": "Response Handle V2 Items In List To V1 Item In List V2 To V1 List Of Items To List Of Items Post",
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+            },
+            "components": {
+                "schemas": pydantic_snapshot(
+                    v1=snapshot(
+                        {
+                            "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": {
+                                "properties": {
+                                    "data1": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList"
+                                        },
+                                        "type": "array",
+                                        "title": "Data1",
+                                    },
+                                    "data2": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList"
+                                        },
+                                        "type": "array",
+                                        "title": "Data2",
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["data1", "data2"],
+                                "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post",
+                            },
+                            "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": {
+                                "properties": {
+                                    "item1": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item"
+                                    },
+                                    "item2": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item"
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["item1", "item2"],
+                                "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post",
+                            },
+                            "HTTPValidationError": {
+                                "properties": {
+                                    "detail": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/ValidationError"
+                                        },
+                                        "type": "array",
+                                        "title": "Detail",
+                                    }
+                                },
+                                "type": "object",
+                                "title": "HTTPValidationError",
+                            },
+                            "ValidationError": {
+                                "properties": {
+                                    "loc": {
+                                        "items": {
+                                            "anyOf": [
+                                                {"type": "string"},
+                                                {"type": "integer"},
+                                            ]
+                                        },
+                                        "type": "array",
+                                        "title": "Location",
+                                    },
+                                    "msg": {"type": "string", "title": "Message"},
+                                    "type": {"type": "string", "title": "Error Type"},
+                                },
+                                "type": "object",
+                                "required": ["loc", "msg", "type"],
+                                "title": "ValidationError",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": {
+                                "properties": {
+                                    "title": {"type": "string", "title": "Title"},
+                                    "size": {"type": "integer", "title": "Size"},
+                                    "description": {
+                                        "type": "string",
+                                        "title": "Description",
+                                    },
+                                    "sub": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem"
+                                    },
+                                    "multi": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem"
+                                        },
+                                        "type": "array",
+                                        "title": "Multi",
+                                        "default": [],
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["title", "size", "sub"],
+                                "title": "Item",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": {
+                                "properties": {
+                                    "name1": {"type": "string", "title": "Name1"}
+                                },
+                                "type": "object",
+                                "required": ["name1"],
+                                "title": "ItemInList",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": {
+                                "properties": {
+                                    "name": {"type": "string", "title": "Name"}
+                                },
+                                "type": "object",
+                                "required": ["name"],
+                                "title": "SubItem",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": {
+                                "properties": {
+                                    "new_title": {
+                                        "type": "string",
+                                        "title": "New Title",
+                                    },
+                                    "new_size": {
+                                        "type": "integer",
+                                        "title": "New Size",
+                                    },
+                                    "new_description": {
+                                        "type": "string",
+                                        "title": "New Description",
+                                    },
+                                    "new_sub": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
+                                    },
+                                    "new_multi": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
+                                        },
+                                        "type": "array",
+                                        "title": "New Multi",
+                                        "default": [],
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["new_title", "new_size", "new_sub"],
+                                "title": "Item",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": {
+                                "properties": {
+                                    "name2": {"type": "string", "title": "Name2"}
+                                },
+                                "type": "object",
+                                "required": ["name2"],
+                                "title": "ItemInList",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": {
+                                "properties": {
+                                    "new_sub_name": {
+                                        "type": "string",
+                                        "title": "New Sub Name",
+                                    }
+                                },
+                                "type": "object",
+                                "required": ["new_sub_name"],
+                                "title": "SubItem",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": {
+                                "properties": {
+                                    "dup_title": {
+                                        "type": "string",
+                                        "title": "Dup Title",
+                                    },
+                                    "dup_size": {
+                                        "type": "integer",
+                                        "title": "Dup Size",
+                                    },
+                                    "dup_description": {
+                                        "type": "string",
+                                        "title": "Dup Description",
+                                    },
+                                    "dup_sub": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem"
+                                    },
+                                    "dup_multi": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem"
+                                        },
+                                        "type": "array",
+                                        "title": "Dup Multi",
+                                        "default": [],
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["dup_title", "dup_size", "dup_sub"],
+                                "title": "Item",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": {
+                                "properties": {
+                                    "dup_name2": {
+                                        "type": "string",
+                                        "title": "Dup Name2",
+                                    }
+                                },
+                                "type": "object",
+                                "required": ["dup_name2"],
+                                "title": "ItemInList",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": {
+                                "properties": {
+                                    "dup_sub_name": {
+                                        "type": "string",
+                                        "title": "Dup Sub Name",
+                                    }
+                                },
+                                "type": "object",
+                                "required": ["dup_sub_name"],
+                                "title": "SubItem",
+                            },
+                        }
+                    ),
+                    v2=snapshot(
+                        {
+                            "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": {
+                                "properties": {
+                                    "data1": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList"
+                                        },
+                                        "type": "array",
+                                        "title": "Data1",
+                                    },
+                                    "data2": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList"
+                                        },
+                                        "type": "array",
+                                        "title": "Data2",
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["data1", "data2"],
+                                "title": "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post",
+                            },
+                            "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post": {
+                                "properties": {
+                                    "item1": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input"
+                                    },
+                                    "item2": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__Item"
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["item1", "item2"],
+                                "title": "Body_handle_v2_same_name_to_v1_v2_to_v1_same_name_post",
+                            },
+                            "HTTPValidationError": {
+                                "properties": {
+                                    "detail": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/ValidationError"
+                                        },
+                                        "type": "array",
+                                        "title": "Detail",
+                                    }
+                                },
+                                "type": "object",
+                                "title": "HTTPValidationError",
+                            },
+                            "SubItem-Output": {
+                                "properties": {
+                                    "new_sub_name": {
+                                        "type": "string",
+                                        "title": "New Sub Name",
+                                    }
+                                },
+                                "type": "object",
+                                "required": ["new_sub_name"],
+                                "title": "SubItem",
+                            },
+                            "ValidationError": {
+                                "properties": {
+                                    "loc": {
+                                        "items": {
+                                            "anyOf": [
+                                                {"type": "string"},
+                                                {"type": "integer"},
+                                            ]
+                                        },
+                                        "type": "array",
+                                        "title": "Location",
+                                    },
+                                    "msg": {"type": "string", "title": "Message"},
+                                    "type": {"type": "string", "title": "Error Type"},
+                                },
+                                "type": "object",
+                                "required": ["loc", "msg", "type"],
+                                "title": "ValidationError",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv1__Item": {
+                                "properties": {
+                                    "title": {"type": "string", "title": "Title"},
+                                    "size": {"type": "integer", "title": "Size"},
+                                    "description": {
+                                        "type": "string",
+                                        "title": "Description",
+                                    },
+                                    "sub": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem"
+                                    },
+                                    "multi": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem"
+                                        },
+                                        "type": "array",
+                                        "title": "Multi",
+                                        "default": [],
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["title", "size", "sub"],
+                                "title": "Item",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": {
+                                "properties": {
+                                    "name1": {"type": "string", "title": "Name1"}
+                                },
+                                "type": "object",
+                                "required": ["name1"],
+                                "title": "ItemInList",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": {
+                                "properties": {
+                                    "name": {"type": "string", "title": "Name"}
+                                },
+                                "type": "object",
+                                "required": ["name"],
+                                "title": "SubItem",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2__Item": {
+                                "properties": {
+                                    "new_title": {
+                                        "type": "string",
+                                        "title": "New Title",
+                                    },
+                                    "new_size": {
+                                        "type": "integer",
+                                        "title": "New Size",
+                                    },
+                                    "new_description": {
+                                        "anyOf": [{"type": "string"}, {"type": "null"}],
+                                        "title": "New Description",
+                                    },
+                                    "new_sub": {
+                                        "$ref": "#/components/schemas/SubItem-Output"
+                                    },
+                                    "new_multi": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/SubItem-Output"
+                                        },
+                                        "type": "array",
+                                        "title": "New Multi",
+                                        "default": [],
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["new_title", "new_size", "new_sub"],
+                                "title": "Item",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input": {
+                                "properties": {
+                                    "new_title": {
+                                        "type": "string",
+                                        "title": "New Title",
+                                    },
+                                    "new_size": {
+                                        "type": "integer",
+                                        "title": "New Size",
+                                    },
+                                    "new_description": {
+                                        "anyOf": [{"type": "string"}, {"type": "null"}],
+                                        "title": "New Description",
+                                    },
+                                    "new_sub": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
+                                    },
+                                    "new_multi": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
+                                        },
+                                        "type": "array",
+                                        "title": "New Multi",
+                                        "default": [],
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["new_title", "new_size", "new_sub"],
+                                "title": "Item",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": {
+                                "properties": {
+                                    "name2": {"type": "string", "title": "Name2"}
+                                },
+                                "type": "object",
+                                "required": ["name2"],
+                                "title": "ItemInList",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem": {
+                                "properties": {
+                                    "new_sub_name": {
+                                        "type": "string",
+                                        "title": "New Sub Name",
+                                    }
+                                },
+                                "type": "object",
+                                "required": ["new_sub_name"],
+                                "title": "SubItem",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2b__Item": {
+                                "properties": {
+                                    "dup_title": {
+                                        "type": "string",
+                                        "title": "Dup Title",
+                                    },
+                                    "dup_size": {
+                                        "type": "integer",
+                                        "title": "Dup Size",
+                                    },
+                                    "dup_description": {
+                                        "anyOf": [{"type": "string"}, {"type": "null"}],
+                                        "title": "Dup Description",
+                                    },
+                                    "dup_sub": {
+                                        "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem"
+                                    },
+                                    "dup_multi": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem"
+                                        },
+                                        "type": "array",
+                                        "title": "Dup Multi",
+                                        "default": [],
+                                    },
+                                },
+                                "type": "object",
+                                "required": ["dup_title", "dup_size", "dup_sub"],
+                                "title": "Item",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2b__ItemInList": {
+                                "properties": {
+                                    "dup_name2": {
+                                        "type": "string",
+                                        "title": "Dup Name2",
+                                    }
+                                },
+                                "type": "object",
+                                "required": ["dup_name2"],
+                                "title": "ItemInList",
+                            },
+                            "tests__test_pydantic_v1_v2_multifile__modelsv2b__SubItem": {
+                                "properties": {
+                                    "dup_sub_name": {
+                                        "type": "string",
+                                        "title": "Dup Sub Name",
+                                    }
+                                },
+                                "type": "object",
+                                "required": ["dup_sub_name"],
+                                "title": "SubItem",
+                            },
+                        }
+                    ),
+                ),
+            },
+        }
+    )
diff --git a/tests/test_pydantic_v1_v2_noneable.py b/tests/test_pydantic_v1_v2_noneable.py
new file mode 100644 (file)
index 0000000..d2d6f66
--- /dev/null
@@ -0,0 +1,766 @@
+import sys
+from typing import Any, List, Union
+
+from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+from fastapi import FastAPI
+from fastapi._compat.v1 import BaseModel
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from pydantic import BaseModel as NewBaseModel
+
+
+class SubItem(BaseModel):
+    name: str
+
+
+class Item(BaseModel):
+    title: str
+    size: int
+    description: Union[str, None] = None
+    sub: SubItem
+    multi: List[SubItem] = []
+
+
+class NewSubItem(NewBaseModel):
+    new_sub_name: str
+
+
+class NewItem(NewBaseModel):
+    new_title: str
+    new_size: int
+    new_description: Union[str, None] = None
+    new_sub: NewSubItem
+    new_multi: List[NewSubItem] = []
+
+
+app = FastAPI()
+
+
+@app.post("/v1-to-v2/")
+def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]:
+    if data.size < 0:
+        return None
+    return NewItem(
+        new_title=data.title,
+        new_size=data.size,
+        new_description=data.description,
+        new_sub=NewSubItem(new_sub_name=data.sub.name),
+        new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
+    )
+
+
+@app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None])
+def handle_v1_item_to_v2_filter(data: Item) -> Any:
+    if data.size < 0:
+        return None
+    result = {
+        "new_title": data.title,
+        "new_size": data.size,
+        "new_description": data.description,
+        "new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"},
+        "new_multi": [
+            {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi
+        ],
+        "secret": "hidden_v1_to_v2",
+    }
+    return result
+
+
+@app.post("/v2-to-v1/item")
+def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]:
+    if data.new_size < 0:
+        return None
+    return Item(
+        title=data.new_title,
+        size=data.new_size,
+        description=data.new_description,
+        sub=SubItem(name=data.new_sub.new_sub_name),
+        multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
+    )
+
+
+@app.post("/v2-to-v1/item-filter", response_model=Union[Item, None])
+def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
+    if data.new_size < 0:
+        return None
+    result = {
+        "title": data.new_title,
+        "size": data.new_size,
+        "description": data.new_description,
+        "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
+        "multi": [
+            {"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi
+        ],
+        "secret": "hidden_v2_to_v1",
+    }
+    return result
+
+
+client = TestClient(app)
+
+
+def test_v1_to_v2_item_success():
+    response = client.post(
+        "/v1-to-v2/",
+        json={
+            "title": "Old Item",
+            "size": 100,
+            "description": "V1 description",
+            "sub": {"name": "V1 Sub"},
+            "multi": [{"name": "M1"}, {"name": "M2"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "new_title": "Old Item",
+        "new_size": 100,
+        "new_description": "V1 description",
+        "new_sub": {"new_sub_name": "V1 Sub"},
+        "new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
+    }
+
+
+def test_v1_to_v2_item_returns_none():
+    response = client.post(
+        "/v1-to-v2/",
+        json={"title": "Invalid Item", "size": -10, "sub": {"name": "Sub"}},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() is None
+
+
+def test_v1_to_v2_item_minimal():
+    response = client.post(
+        "/v1-to-v2/", json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}}
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "new_title": "Minimal",
+        "new_size": 50,
+        "new_description": None,
+        "new_sub": {"new_sub_name": "MinSub"},
+        "new_multi": [],
+    }
+
+
+def test_v1_to_v2_item_filter_success():
+    response = client.post(
+        "/v1-to-v2/item-filter",
+        json={
+            "title": "Filtered Item",
+            "size": 50,
+            "sub": {"name": "Sub"},
+            "multi": [{"name": "Multi1"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result["new_title"] == "Filtered Item"
+    assert result["new_size"] == 50
+    assert result["new_sub"]["new_sub_name"] == "Sub"
+    assert result["new_multi"][0]["new_sub_name"] == "Multi1"
+    # Verify secret fields are filtered out
+    assert "secret" not in result
+    assert "new_sub_secret" not in result["new_sub"]
+    assert "new_sub_secret" not in result["new_multi"][0]
+
+
+def test_v1_to_v2_item_filter_returns_none():
+    response = client.post(
+        "/v1-to-v2/item-filter",
+        json={"title": "Invalid", "size": -1, "sub": {"name": "Sub"}},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() is None
+
+
+def test_v2_to_v1_item_success():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "New Item",
+            "new_size": 200,
+            "new_description": "V2 description",
+            "new_sub": {"new_sub_name": "V2 Sub"},
+            "new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "New Item",
+        "size": 200,
+        "description": "V2 description",
+        "sub": {"name": "V2 Sub"},
+        "multi": [{"name": "N1"}, {"name": "N2"}],
+    }
+
+
+def test_v2_to_v1_item_returns_none():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "Invalid New",
+            "new_size": -5,
+            "new_sub": {"new_sub_name": "NewSub"},
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() is None
+
+
+def test_v2_to_v1_item_minimal():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "MinimalNew",
+            "new_size": 75,
+            "new_sub": {"new_sub_name": "MinNewSub"},
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "title": "MinimalNew",
+        "size": 75,
+        "description": None,
+        "sub": {"name": "MinNewSub"},
+        "multi": [],
+    }
+
+
+def test_v2_to_v1_item_filter_success():
+    response = client.post(
+        "/v2-to-v1/item-filter",
+        json={
+            "new_title": "Filtered New",
+            "new_size": 75,
+            "new_sub": {"new_sub_name": "NewSub"},
+            "new_multi": [],
+        },
+    )
+    assert response.status_code == 200, response.text
+    result = response.json()
+    assert result["title"] == "Filtered New"
+    assert result["size"] == 75
+    assert result["sub"]["name"] == "NewSub"
+    # Verify secret fields are filtered out
+    assert "secret" not in result
+    assert "sub_secret" not in result["sub"]
+
+
+def test_v2_to_v1_item_filter_returns_none():
+    response = client.post(
+        "/v2-to-v1/item-filter",
+        json={
+            "new_title": "Invalid Filtered",
+            "new_size": -100,
+            "new_sub": {"new_sub_name": "Sub"},
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() is None
+
+
+def test_v1_to_v2_validation_error():
+    response = client.post("/v1-to-v2/", json={"title": "Missing fields"})
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "loc": ["body", "size"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "sub"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
+
+
+def test_v1_to_v2_nested_validation_error():
+    response = client.post(
+        "/v1-to-v2/",
+        json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}},
+    )
+    assert response.status_code == 422, response.text
+    error_detail = response.json()["detail"]
+    assert len(error_detail) == 1
+    assert error_detail[0]["loc"] == ["body", "sub", "name"]
+
+
+def test_v1_to_v2_type_validation_error():
+    response = client.post(
+        "/v1-to-v2/",
+        json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}},
+    )
+    assert response.status_code == 422, response.text
+    error_detail = response.json()["detail"]
+    assert len(error_detail) == 1
+    assert error_detail[0]["loc"] == ["body", "size"]
+
+
+def test_v2_to_v1_validation_error():
+    response = client.post("/v2-to-v1/item", json={"new_title": "Missing fields"})
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": pydantic_snapshot(
+                v2=snapshot(
+                    [
+                        {
+                            "type": "missing",
+                            "loc": ["body", "new_size"],
+                            "msg": "Field required",
+                            "input": {"new_title": "Missing fields"},
+                        },
+                        {
+                            "type": "missing",
+                            "loc": ["body", "new_sub"],
+                            "msg": "Field required",
+                            "input": {"new_title": "Missing fields"},
+                        },
+                    ]
+                ),
+                v1=snapshot(
+                    [
+                        {
+                            "loc": ["body", "new_size"],
+                            "msg": "field required",
+                            "type": "value_error.missing",
+                        },
+                        {
+                            "loc": ["body", "new_sub"],
+                            "msg": "field required",
+                            "type": "value_error.missing",
+                        },
+                    ]
+                ),
+            )
+        }
+    )
+
+
+def test_v2_to_v1_nested_validation_error():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "Bad sub",
+            "new_size": 200,
+            "new_sub": {"wrong_field": "value"},
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                pydantic_snapshot(
+                    v2=snapshot(
+                        {
+                            "type": "missing",
+                            "loc": ["body", "new_sub", "new_sub_name"],
+                            "msg": "Field required",
+                            "input": {"wrong_field": "value"},
+                        }
+                    ),
+                    v1=snapshot(
+                        {
+                            "loc": ["body", "new_sub", "new_sub_name"],
+                            "msg": "field required",
+                            "type": "value_error.missing",
+                        }
+                    ),
+                )
+            ]
+        }
+    )
+
+
+def test_v2_to_v1_type_validation_error():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "Bad type",
+            "new_size": "not_a_number",
+            "new_sub": {"new_sub_name": "Sub"},
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                pydantic_snapshot(
+                    v2=snapshot(
+                        {
+                            "type": "int_parsing",
+                            "loc": ["body", "new_size"],
+                            "msg": "Input should be a valid integer, unable to parse string as an integer",
+                            "input": "not_a_number",
+                        }
+                    ),
+                    v1=snapshot(
+                        {
+                            "loc": ["body", "new_size"],
+                            "msg": "value is not a valid integer",
+                            "type": "type_error.integer",
+                        }
+                    ),
+                )
+            ]
+        }
+    )
+
+
+def test_v1_to_v2_with_multi_items():
+    response = client.post(
+        "/v1-to-v2/",
+        json={
+            "title": "Complex Item",
+            "size": 300,
+            "description": "Item with multiple sub-items",
+            "sub": {"name": "Main Sub"},
+            "multi": [{"name": "Sub1"}, {"name": "Sub2"}, {"name": "Sub3"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "new_title": "Complex Item",
+            "new_size": 300,
+            "new_description": "Item with multiple sub-items",
+            "new_sub": {"new_sub_name": "Main Sub"},
+            "new_multi": [
+                {"new_sub_name": "Sub1"},
+                {"new_sub_name": "Sub2"},
+                {"new_sub_name": "Sub3"},
+            ],
+        }
+    )
+
+
+def test_v2_to_v1_with_multi_items():
+    response = client.post(
+        "/v2-to-v1/item",
+        json={
+            "new_title": "Complex New Item",
+            "new_size": 400,
+            "new_description": "New item with multiple sub-items",
+            "new_sub": {"new_sub_name": "Main New Sub"},
+            "new_multi": [{"new_sub_name": "NewSub1"}, {"new_sub_name": "NewSub2"}],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "title": "Complex New Item",
+            "size": 400,
+            "description": "New item with multiple sub-items",
+            "sub": {"name": "Main New Sub"},
+            "multi": [{"name": "NewSub1"}, {"name": "NewSub2"}],
+        }
+    )
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/v1-to-v2/": {
+                    "post": {
+                        "summary": "Handle V1 Item To V2",
+                        "operationId": "handle_v1_item_to_v2_v1_to_v2__post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": pydantic_snapshot(
+                                            v2=snapshot(
+                                                {
+                                                    "anyOf": [
+                                                        {
+                                                            "$ref": "#/components/schemas/NewItem"
+                                                        },
+                                                        {"type": "null"},
+                                                    ],
+                                                    "title": "Response Handle V1 Item To V2 V1 To V2  Post",
+                                                }
+                                            ),
+                                            v1=snapshot(
+                                                {"$ref": "#/components/schemas/NewItem"}
+                                            ),
+                                        )
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v1-to-v2/item-filter": {
+                    "post": {
+                        "summary": "Handle V1 Item To V2 Filter",
+                        "operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": pydantic_snapshot(
+                                        v2=snapshot(
+                                            {
+                                                "allOf": [
+                                                    {
+                                                        "$ref": "#/components/schemas/Item"
+                                                    }
+                                                ],
+                                                "title": "Data",
+                                            }
+                                        ),
+                                        v1=snapshot(
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ),
+                                    )
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": pydantic_snapshot(
+                                            v2=snapshot(
+                                                {
+                                                    "anyOf": [
+                                                        {
+                                                            "$ref": "#/components/schemas/NewItem"
+                                                        },
+                                                        {"type": "null"},
+                                                    ],
+                                                    "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post",
+                                                }
+                                            ),
+                                            v1=snapshot(
+                                                {"$ref": "#/components/schemas/NewItem"}
+                                            ),
+                                        )
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/item": {
+                    "post": {
+                        "summary": "Handle V2 Item To V1",
+                        "operationId": "handle_v2_item_to_v1_v2_to_v1_item_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/NewItem"}
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+                "/v2-to-v1/item-filter": {
+                    "post": {
+                        "summary": "Handle V2 Item To V1 Filter",
+                        "operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/NewItem"}
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                },
+            },
+            "components": {
+                "schemas": {
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
+                        },
+                        "type": "object",
+                        "title": "HTTPValidationError",
+                    },
+                    "Item": {
+                        "properties": {
+                            "title": {"type": "string", "title": "Title"},
+                            "size": {"type": "integer", "title": "Size"},
+                            "description": {"type": "string", "title": "Description"},
+                            "sub": {"$ref": "#/components/schemas/SubItem"},
+                            "multi": {
+                                "items": {"$ref": "#/components/schemas/SubItem"},
+                                "type": "array",
+                                "title": "Multi",
+                                "default": [],
+                            },
+                        },
+                        "type": "object",
+                        "required": ["title", "size", "sub"],
+                        "title": "Item",
+                    },
+                    "NewItem": {
+                        "properties": {
+                            "new_title": {"type": "string", "title": "New Title"},
+                            "new_size": {"type": "integer", "title": "New Size"},
+                            "new_description": pydantic_snapshot(
+                                v2=snapshot(
+                                    {
+                                        "anyOf": [{"type": "string"}, {"type": "null"}],
+                                        "title": "New Description",
+                                    }
+                                ),
+                                v1=snapshot(
+                                    {"type": "string", "title": "New Description"}
+                                ),
+                            ),
+                            "new_sub": {"$ref": "#/components/schemas/NewSubItem"},
+                            "new_multi": {
+                                "items": {"$ref": "#/components/schemas/NewSubItem"},
+                                "type": "array",
+                                "title": "New Multi",
+                                "default": [],
+                            },
+                        },
+                        "type": "object",
+                        "required": ["new_title", "new_size", "new_sub"],
+                        "title": "NewItem",
+                    },
+                    "NewSubItem": {
+                        "properties": {
+                            "new_sub_name": {"type": "string", "title": "New Sub Name"}
+                        },
+                        "type": "object",
+                        "required": ["new_sub_name"],
+                        "title": "NewSubItem",
+                    },
+                    "SubItem": {
+                        "properties": {"name": {"type": "string", "title": "Name"}},
+                        "type": "object",
+                        "required": ["name"],
+                        "title": "SubItem",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
+                            },
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
+                        },
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                    },
+                }
+            },
+        }
+    )
index 6948430a13b56d63bc5f91e8017894147615cf25..c3c0ed6c4a08021ba2f883a0b3d4fd2eae027345 100644 (file)
@@ -2,6 +2,7 @@ from typing import List, Union
 
 import pytest
 from fastapi import FastAPI
+from fastapi._compat import v1
 from fastapi.exceptions import FastAPIError, ResponseValidationError
 from fastapi.responses import JSONResponse, Response
 from fastapi.testclient import TestClient
@@ -509,6 +510,23 @@ def test_invalid_response_model_field():
     assert "parameter response_model=None" in e.value.args[0]
 
 
+# TODO: remove when dropping Pydantic v1 support
+def test_invalid_response_model_field_pv1():
+    app = FastAPI()
+
+    class Model(v1.BaseModel):
+        foo: str
+
+    with pytest.raises(FastAPIError) as e:
+
+        @app.get("/")
+        def read_root() -> Union[Response, Model, None]:
+            return Response(content="Foo")  # pragma: no cover
+
+    assert "valid Pydantic field type" in e.value.args[0]
+    assert "parameter response_model=None" in e.value.args[0]
+
+
 def test_openapi_schema():
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py b/tests/test_tutorial/test_pydantic_v1_in_v2/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial001.py
new file mode 100644 (file)
index 0000000..3075a05
--- /dev/null
@@ -0,0 +1,37 @@
+import sys
+from typing import Any
+
+import pytest
+from fastapi._compat import PYDANTIC_V2
+
+from tests.utils import skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+
+if not PYDANTIC_V2:
+    pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
+
+import importlib
+
+import pytest
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="mod",
+    params=[
+        "tutorial001_an",
+        pytest.param("tutorial001_an_py310", marks=needs_py310),
+    ],
+)
+def get_mod(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
+    return mod
+
+
+def test_model(mod: Any):
+    item = mod.Item(name="Foo", size=3.4)
+    assert item.dict() == {"name": "Foo", "description": None, "size": 3.4}
diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial002.py
new file mode 100644 (file)
index 0000000..a402c66
--- /dev/null
@@ -0,0 +1,140 @@
+import sys
+
+import pytest
+from fastapi._compat import PYDANTIC_V2
+from inline_snapshot import snapshot
+
+from tests.utils import skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+
+if not PYDANTIC_V2:
+    pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
+
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        "tutorial002_an",
+        pytest.param("tutorial002_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
+
+    c = TestClient(mod.app)
+    return c
+
+
+def test_call(client: TestClient):
+    response = client.post("/items/", json={"name": "Foo", "size": 3.4})
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Foo",
+        "description": None,
+        "size": 3.4,
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/items/": {
+                    "post": {
+                        "summary": "Create Item",
+                        "operationId": "create_item_items__post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                }
+            },
+            "components": {
+                "schemas": {
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
+                        },
+                        "type": "object",
+                        "title": "HTTPValidationError",
+                    },
+                    "Item": {
+                        "properties": {
+                            "name": {"type": "string", "title": "Name"},
+                            "description": {"type": "string", "title": "Description"},
+                            "size": {"type": "number", "title": "Size"},
+                        },
+                        "type": "object",
+                        "required": ["name", "size"],
+                        "title": "Item",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
+                            },
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
+                        },
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                    },
+                }
+            },
+        }
+    )
diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial003.py
new file mode 100644 (file)
index 0000000..03155c9
--- /dev/null
@@ -0,0 +1,154 @@
+import sys
+
+import pytest
+from fastapi._compat import PYDANTIC_V2
+from inline_snapshot import snapshot
+
+from tests.utils import skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+if not PYDANTIC_V2:
+    pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
+
+
+import importlib
+
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        "tutorial003_an",
+        pytest.param("tutorial003_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
+
+    c = TestClient(mod.app)
+    return c
+
+
+def test_call(client: TestClient):
+    response = client.post("/items/", json={"name": "Foo", "size": 3.4})
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Foo",
+        "description": None,
+        "size": 3.4,
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/items/": {
+                    "post": {
+                        "summary": "Create Item",
+                        "operationId": "create_item_items__post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/ItemV2"
+                                        }
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                }
+            },
+            "components": {
+                "schemas": {
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
+                        },
+                        "type": "object",
+                        "title": "HTTPValidationError",
+                    },
+                    "Item": {
+                        "properties": {
+                            "name": {"type": "string", "title": "Name"},
+                            "description": {"type": "string", "title": "Description"},
+                            "size": {"type": "number", "title": "Size"},
+                        },
+                        "type": "object",
+                        "required": ["name", "size"],
+                        "title": "Item",
+                    },
+                    "ItemV2": {
+                        "properties": {
+                            "name": {"type": "string", "title": "Name"},
+                            "description": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Description",
+                            },
+                            "size": {"type": "number", "title": "Size"},
+                        },
+                        "type": "object",
+                        "required": ["name", "size"],
+                        "title": "ItemV2",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
+                            },
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
+                        },
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                    },
+                }
+            },
+        }
+    )
diff --git a/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py b/tests/test_tutorial/test_pydantic_v1_in_v2/test_tutorial004.py
new file mode 100644 (file)
index 0000000..d2e204d
--- /dev/null
@@ -0,0 +1,153 @@
+import sys
+
+import pytest
+from fastapi._compat import PYDANTIC_V2
+from inline_snapshot import snapshot
+
+from tests.utils import skip_module_if_py_gte_314
+
+if sys.version_info >= (3, 14):
+    skip_module_if_py_gte_314()
+
+if not PYDANTIC_V2:
+    pytest.skip("This test is only for Pydantic v2", allow_module_level=True)
+
+
+import importlib
+
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py39, needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        "tutorial004_an",
+        pytest.param("tutorial004_an_py39", marks=needs_py39),
+        pytest.param("tutorial004_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.pydantic_v1_in_v2.{request.param}")
+
+    c = TestClient(mod.app)
+    return c
+
+
+def test_call(client: TestClient):
+    response = client.post("/items/", json={"item": {"name": "Foo", "size": 3.4}})
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Foo",
+        "description": None,
+        "size": 3.4,
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/items/": {
+                    "post": {
+                        "summary": "Create Item",
+                        "operationId": "create_item_items__post",
+                        "requestBody": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_item_items__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                }
+                            },
+                            "required": True,
+                        },
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {"$ref": "#/components/schemas/Item"}
+                                    }
+                                },
+                            },
+                            "422": {
+                                "description": "Validation Error",
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError"
+                                        }
+                                    }
+                                },
+                            },
+                        },
+                    }
+                }
+            },
+            "components": {
+                "schemas": {
+                    "Body_create_item_items__post": {
+                        "properties": {
+                            "item": {
+                                "allOf": [{"$ref": "#/components/schemas/Item"}],
+                                "title": "Item",
+                            }
+                        },
+                        "type": "object",
+                        "required": ["item"],
+                        "title": "Body_create_item_items__post",
+                    },
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "type": "array",
+                                "title": "Detail",
+                            }
+                        },
+                        "type": "object",
+                        "title": "HTTPValidationError",
+                    },
+                    "Item": {
+                        "properties": {
+                            "name": {"type": "string", "title": "Name"},
+                            "description": {"type": "string", "title": "Description"},
+                            "size": {"type": "number", "title": "Size"},
+                        },
+                        "type": "object",
+                        "required": ["name", "size"],
+                        "title": "Item",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}]
+                                },
+                                "type": "array",
+                                "title": "Location",
+                            },
+                            "msg": {"type": "string", "title": "Message"},
+                            "type": {"type": "string", "title": "Error Type"},
+                        },
+                        "type": "object",
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                    },
+                }
+            },
+        }
+    )
index ae9543e3b7b8c223006ecb09b35c04572913e261..691e92bbfbdf3a447e38d1ba960d9faafc22d9d8 100644 (file)
@@ -8,10 +8,19 @@ needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires pyth
 needs_py310 = pytest.mark.skipif(
     sys.version_info < (3, 10), reason="requires python3.10+"
 )
+needs_py_lt_314 = pytest.mark.skipif(
+    sys.version_info > (3, 13), reason="requires python3.13-"
+)
 needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2")
 needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1")
 
 
+def skip_module_if_py_gte_314():
+    """Skip entire module on Python 3.14+ at import time."""
+    if sys.version_info >= (3, 14):
+        pytest.skip("requires python3.13-", allow_module_level=True)
+
+
 def pydantic_snapshot(
     *,
     v2: Snapshot,