Merge branch 'master' into deferred-init

This commit is contained in:
Jan Vollmer 2025-12-22 13:36:10 +01:00
commit 1066dff16d
No known key found for this signature in database
GPG Key ID: 19473D3A5AB433DA
154 changed files with 2270 additions and 4273 deletions

View File

@ -60,8 +60,6 @@ jobs:
pyproject.toml pyproject.toml
- name: Install docs extras - name: Install docs extras
run: uv pip install -r requirements-docs.txt run: uv pip install -r requirements-docs.txt
- name: Verify Docs
run: python ./scripts/docs.py verify-docs
- name: Export Language Codes - name: Export Language Codes
id: show-langs id: show-langs
run: | run: |

View File

@ -21,14 +21,21 @@ jobs:
name: Checkout PR for own repo name: Checkout PR for own repo
if: env.IS_FORK == 'false' if: env.IS_FORK == 'false'
with: with:
# To be able to commit it needs more than the last commit # To be able to commit it needs to fetch the head of the branch, not the
# merge commit
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}
# And it needs the full history to be able to compute diffs
fetch-depth: 0
# A token other than the default GITHUB_TOKEN is needed to be able to trigger CI # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI
token: ${{ secrets.PRE_COMMIT }} token: ${{ secrets.PRE_COMMIT }}
# pre-commit lite ci needs the default checkout configs to work # pre-commit lite ci needs the default checkout configs to work
- uses: actions/checkout@v5 - uses: actions/checkout@v5
name: Checkout PR for fork name: Checkout PR for fork
if: env.IS_FORK == 'true' if: env.IS_FORK == 'true'
with:
# To be able to commit it needs the head branch of the PR, the remote one
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@ -44,12 +51,9 @@ jobs:
run: | run: |
uv venv uv venv
uv pip install -r requirements.txt uv pip install -r requirements.txt
- name: Run pre-commit - name: Run prek - pre-commit
id: precommit id: precommit
run: | run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure
# Fetch the base branch for comparison
git fetch origin ${{ github.base_ref }}
uvx pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --show-diff-on-failure
continue-on-error: true continue-on-error: true
- name: Commit and push changes - name: Commit and push changes
if: env.IS_FORK == 'false' if: env.IS_FORK == 'false'

View File

@ -44,35 +44,26 @@ jobs:
matrix: matrix:
os: [ windows-latest, macos-latest ] os: [ windows-latest, macos-latest ]
python-version: [ "3.14" ] python-version: [ "3.14" ]
pydantic-version: [ "pydantic>=2.0.2,<3.0.0" ]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
python-version: "3.9" python-version: "3.9"
pydantic-version: "pydantic>=1.10.0,<2.0.0"
coverage: coverage coverage: coverage
- os: macos-latest - os: macos-latest
python-version: "3.10" python-version: "3.10"
pydantic-version: "pydantic>=2.0.2,<3.0.0" coverage: coverage
- os: windows-latest - os: windows-latest
python-version: "3.11"
pydantic-version: "pydantic>=1.10.0,<2.0.0"
- os: ubuntu-latest
python-version: "3.12" python-version: "3.12"
pydantic-version: "pydantic>=2.0.2,<3.0.0"
- os: macos-latest
python-version: "3.13"
pydantic-version: "pydantic>=1.10.0,<2.0.0"
- os: windows-latest
python-version: "3.13"
pydantic-version: "pydantic>=2.0.2,<3.0.0"
coverage: coverage coverage: coverage
- os: ubuntu-latest - os: ubuntu-latest
python-version: "3.13" python-version: "3.13"
pydantic-version: "pydantic>=2.0.2,<3.0.0"
coverage: coverage coverage: coverage
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
- os: ubuntu-latest
python-version: "3.13"
coverage: coverage
codspeed: codspeed
- os: ubuntu-latest - os: ubuntu-latest
python-version: "3.14" python-version: "3.14"
pydantic-version: "pydantic>=2.0.2,<3.0.0"
coverage: coverage coverage: coverage
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -96,16 +87,15 @@ jobs:
pyproject.toml pyproject.toml
- name: Install Dependencies - name: Install Dependencies
run: uv pip install -r requirements-tests.txt run: uv pip install -r requirements-tests.txt
- name: Install Pydantic
run: uv pip install "${{ matrix.pydantic-version }}"
- run: mkdir coverage - run: mkdir coverage
- name: Test - name: Test
if: matrix.codspeed != 'codspeed'
run: bash scripts/test.sh run: bash scripts/test.sh
env: env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
- name: CodSpeed benchmarks - name: CodSpeed benchmarks
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && matrix.pydantic-version == 'pydantic>=2.0.2,<3.0.0' if: matrix.codspeed == 'codspeed'
uses: CodSpeedHQ/action@v4 uses: CodSpeedHQ/action@v4
env: env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}

View File

@ -5,6 +5,7 @@ repos:
rev: v6.0.0 rev: v6.0.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
args: ['--maxkb=750']
- id: check-toml - id: check-toml
- id: check-yaml - id: check-yaml
args: args:
@ -20,10 +21,28 @@ repos:
- id: ruff-format - id: ruff-format
- repo: local - repo: local
hooks: hooks:
- id: local-script - id: add-permalinks-pages
language: unsupported language: unsupported
name: local script name: add-permalinks-pages
entry: uv run ./scripts/docs.py add-permalinks-pages entry: uv run ./scripts/docs.py add-permalinks-pages
args: args:
- --update-existing - --update-existing
files: ^docs/en/docs/.*\.md$ files: ^docs/en/docs/.*\.md$
- id: generate-readme
language: unsupported
name: generate README.md from index.md
entry: uv run ./scripts/docs.py generate-readme
files: ^docs/en/docs/index\.md|docs/en/data/sponsors\.yml|scripts/docs\.py$
pass_filenames: false
- id: update-languages
language: unsupported
name: update languages
entry: uv run ./scripts/docs.py update-languages
files: ^docs/.*|scripts/docs\.py$
pass_filenames: false
- id: ensure-non-translated
language: unsupported
name: ensure non-translated files are not modified
entry: uv run ./scripts/docs.py ensure-non-translated
files: ^docs/(?!en/).*|^scripts/docs\.py$
pass_filenames: false

View File

@ -153,48 +153,16 @@ And you could do this even if the data type in the request is not JSON.
For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON: For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON:
//// tab | Pydantic v2
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *} {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
////
//// tab | Pydantic v1
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *}
////
/// info
In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_json_schema()`.
///
Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML. Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML.
Then we use the request directly, and extract the body as `bytes`. This means that FastAPI won't even try to parse the request payload as JSON. Then we use the request directly, and extract the body as `bytes`. This means that FastAPI won't even try to parse the request payload as JSON.
And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content: And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content:
//// tab | Pydantic v2
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *} {* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
////
//// tab | Pydantic v1
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *}
////
/// info
In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`.
///
/// tip /// tip
Here we reuse the same Pydantic model. Here we reuse the same Pydantic model.

View File

@ -46,12 +46,6 @@ $ pip install "fastapi[all]"
</div> </div>
/// info
In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality.
///
### Create the `Settings` object { #create-the-settings-object } ### Create the `Settings` object { #create-the-settings-object }
Import `BaseSettings` from Pydantic and create a sub-class, very much like with a Pydantic model. Import `BaseSettings` from Pydantic and create a sub-class, very much like with a Pydantic model.
@ -60,24 +54,8 @@ The same way as with Pydantic models, you declare class attributes with type ann
You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`. You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`.
//// tab | Pydantic v2
{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *} {* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *}
////
//// tab | Pydantic v1
/// info
In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`.
///
{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *}
////
/// tip /// tip
If you want something quick to copy and paste, don't use this example, use the last one below. If you want something quick to copy and paste, don't use this example, use the last one below.
@ -215,8 +193,6 @@ APP_NAME="ChimichangApp"
And then update your `config.py` with: And then update your `config.py` with:
//// tab | Pydantic v2
{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *} {* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
/// tip /// tip
@ -225,26 +201,6 @@ The `model_config` attribute is used just for Pydantic configuration. You can re
/// ///
////
//// tab | Pydantic v1
{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *}
/// tip
The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
///
////
/// info
In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`.
///
Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use. Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use.
### Creating the `Settings` only once with `lru_cache` { #creating-the-settings-only-once-with-lru-cache } ### Creating the `Settings` only once with `lru_cache` { #creating-the-settings-only-once-with-lru-cache }

View File

@ -2,21 +2,23 @@
If you have an old FastAPI app, you might be using Pydantic version 1. 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. FastAPI version 0.100.0 had support for either Pydantic v1 or v2. It would use whichever you had installed.
If you had installed Pydantic v2, it would use it. If instead you had Pydantic v1, it would use that. FastAPI version 0.119.0 introduced partial support for Pydantic v1 from inside of Pydantic v2 (as `pydantic.v1`), to facilitate the migration to v2.
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. FastAPI 0.126.0 dropped support for Pydantic v1, while still supporting `pydantic.v1` for a little while.
/// warning /// warning
Also, the Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**. The Pydantic team stopped support for Pydantic v1 for the latest versions of Python, starting with **Python 3.14**.
This includes `pydantic.v1`, which is no longer supported in Python 3.14 and above.
If you want to use the latest features of Python, you will need to make sure you use Pydantic v2. 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. If you have an old FastAPI app with Pydantic v1, here I'll show you how to migrate it to Pydantic v2, and the **features in FastAPI 0.119.0** to help you with a gradual migration.
## Official Guide { #official-guide } ## Official Guide { #official-guide }
@ -44,7 +46,7 @@ After this, you can run the tests and check if everything works. If it does, you
## Pydantic v1 in v2 { #pydantic-v1-in-v2 } ## Pydantic v1 in v2 { #pydantic-v1-in-v2 }
Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`. Pydantic v2 includes everything from Pydantic v1 as a submodule `pydantic.v1`. But this is no longer supported in versions above Python 3.13.
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. 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.

View File

@ -1,6 +1,6 @@
# Separate OpenAPI Schemas for Input and Output or Not { #separate-openapi-schemas-for-input-and-output-or-not } # Separate OpenAPI Schemas for Input and Output or Not { #separate-openapi-schemas-for-input-and-output-or-not }
When using **Pydantic v2**, the generated OpenAPI is a bit more exact and **correct** than before. 😎 Since **Pydantic v2** was released, the generated OpenAPI is a bit more exact and **correct** than before. 😎
In fact, in some cases, it will even have **two JSON Schemas** in OpenAPI for the same Pydantic model, for input and output, depending on if they have **default values**. In fact, in some cases, it will even have **two JSON Schemas** in OpenAPI for the same Pydantic model, for input and output, depending on if they have **default values**.
@ -100,5 +100,3 @@ And now there will be one single schema for input and output for the model, only
<div class="screenshot"> <div class="screenshot">
<img src="/img/tutorial/separate-openapi-schemas/image05.png"> <img src="/img/tutorial/separate-openapi-schemas/image05.png">
</div> </div>
This is the same behavior as in Pydantic v1. 🤓

View File

@ -9,6 +9,44 @@ hide:
### Internal ### Internal
* 👷 Run CodSpeed tests in parallel to other tests to speed up CI. PR [#14586](https://github.com/fastapi/fastapi/pull/14586) by [@tiangolo](https://github.com/tiangolo).
* 🔨 Update scripts and pre-commit to autofix files. PR [#14585](https://github.com/fastapi/fastapi/pull/14585) by [@tiangolo](https://github.com/tiangolo).
## 0.127.0
### Breaking Changes
* 🔊 Add deprecation warnings when using `pydantic.v1`. PR [#14583](https://github.com/fastapi/fastapi/pull/14583) by [@tiangolo](https://github.com/tiangolo).
### Translations
* 🔧 Add LLM prompt file for Korean, generated from the existing translations. PR [#14546](https://github.com/fastapi/fastapi/pull/14546) by [@tiangolo](https://github.com/tiangolo).
* 🔧 Add LLM prompt file for Japanese, generated from the existing translations. PR [#14545](https://github.com/fastapi/fastapi/pull/14545) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⬆️ Upgrade OpenAI model for translations to gpt-5.2. PR [#14579](https://github.com/fastapi/fastapi/pull/14579) by [@tiangolo](https://github.com/tiangolo).
## 0.126.0
### Upgrades
* Drop support for Pydantic v1, keeping short temporary support for Pydantic v2's `pydantic.v1`. PR [#14575](https://github.com/fastapi/fastapi/pull/14575) by [@tiangolo](https://github.com/tiangolo).
* The minimum version of Pydantic installed is now `pydantic >=2.7.0`.
* The `standard` dependencies now include `pydantic-settings >=2.0.0` and `pydantic-extra-types >=2.0.0`.
### Docs
* 📝 Fix duplicated variable in `docs_src/python_types/tutorial005_py39.py`. PR [#14565](https://github.com/fastapi/fastapi/pull/14565) by [@paras-verma7454](https://github.com/paras-verma7454).
### Translations
* 🔧 Add LLM prompt file for Ukrainian, generated from the existing translations. PR [#14548](https://github.com/fastapi/fastapi/pull/14548) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 🔧 Tweak pre-commit to allow committing release-notes. PR [#14577](https://github.com/fastapi/fastapi/pull/14577) by [@tiangolo](https://github.com/tiangolo).
* ⬆️ Use prek as a pre-commit alternative. PR [#14572](https://github.com/fastapi/fastapi/pull/14572) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add performance tests with CodSpeed. PR [#14558](https://github.com/fastapi/fastapi/pull/14558) by [@tiangolo](https://github.com/tiangolo). * 👷 Add performance tests with CodSpeed. PR [#14558](https://github.com/fastapi/fastapi/pull/14558) by [@tiangolo](https://github.com/tiangolo).
## 0.125.0 ## 0.125.0

View File

@ -50,14 +50,6 @@ If you want to receive partial updates, it's very useful to use the parameter `e
Like `item.model_dump(exclude_unset=True)`. Like `item.model_dump(exclude_unset=True)`.
/// info
In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`.
The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2.
///
That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values. That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values.
Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values: Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values:
@ -68,14 +60,6 @@ Then you can use this to generate a `dict` with only the data that was set (sent
Now, you can create a copy of the existing model using `.model_copy()`, and pass the `update` parameter with a `dict` containing the data to update. Now, you can create a copy of the existing model using `.model_copy()`, and pass the `update` parameter with a `dict` containing the data to update.
/// info
In Pydantic v1 the method was called `.copy()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_copy()`.
The examples here use `.copy()` for compatibility with Pydantic v1, but you should use `.model_copy()` instead if you can use Pydantic v2.
///
Like `stored_item_model.model_copy(update=update_data)`: Like `stored_item_model.model_copy(update=update_data)`:
{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} {* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *}

View File

@ -128,14 +128,6 @@ Inside of the function, you can access all the attributes of the model object di
{* ../../docs_src/body/tutorial002_py310.py *} {* ../../docs_src/body/tutorial002_py310.py *}
/// info
In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`.
The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2.
///
## Request body + path parameters { #request-body-path-parameters } ## Request body + path parameters { #request-body-path-parameters }
You can declare path parameters and request body at the same time. You can declare path parameters and request body at the same time.

View File

@ -22,22 +22,13 @@ Here's a general idea of how the models could look like with their password fiel
{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} {* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *}
### About `**user_in.model_dump()` { #about-user-in-model-dump }
/// info #### Pydantic's `.model_dump()` { #pydantics-model-dump }
In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`.
The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2.
///
### About `**user_in.dict()` { #about-user-in-dict }
#### Pydantic's `.dict()` { #pydantics-dict }
`user_in` is a Pydantic model of class `UserIn`. `user_in` is a Pydantic model of class `UserIn`.
Pydantic models have a `.dict()` method that returns a `dict` with the model's data. Pydantic models have a `.model_dump()` method that returns a `dict` with the model's data.
So, if we create a Pydantic object `user_in` like: So, if we create a Pydantic object `user_in` like:
@ -48,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com
and then we call: and then we call:
```Python ```Python
user_dict = user_in.dict() user_dict = user_in.model_dump()
``` ```
we now have a `dict` with the data in the variable `user_dict` (it's a `dict` instead of a Pydantic model object). we now have a `dict` with the data in the variable `user_dict` (it's a `dict` instead of a Pydantic model object).
@ -104,20 +95,20 @@ UserInDB(
#### A Pydantic model from the contents of another { #a-pydantic-model-from-the-contents-of-another } #### A Pydantic model from the contents of another { #a-pydantic-model-from-the-contents-of-another }
As in the example above we got `user_dict` from `user_in.dict()`, this code: As in the example above we got `user_dict` from `user_in.model_dump()`, this code:
```Python ```Python
user_dict = user_in.dict() user_dict = user_in.model_dump()
UserInDB(**user_dict) UserInDB(**user_dict)
``` ```
would be equivalent to: would be equivalent to:
```Python ```Python
UserInDB(**user_in.dict()) UserInDB(**user_in.model_dump())
``` ```
...because `user_in.dict()` is a `dict`, and then we make Python "unpack" it by passing it to `UserInDB` prefixed with `**`. ...because `user_in.model_dump()` is a `dict`, and then we make Python "unpack" it by passing it to `UserInDB` prefixed with `**`.
So, we get a Pydantic model from the data in another Pydantic model. So, we get a Pydantic model from the data in another Pydantic model.
@ -126,7 +117,7 @@ So, we get a Pydantic model from the data in another Pydantic model.
And then adding the extra keyword argument `hashed_password=hashed_password`, like in: And then adding the extra keyword argument `hashed_password=hashed_password`, like in:
```Python ```Python
UserInDB(**user_in.dict(), hashed_password=hashed_password) UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
``` ```
...ends up being like: ...ends up being like:
@ -181,7 +172,6 @@ When defining a <a href="https://docs.pydantic.dev/latest/concepts/types/#unions
{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} {* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *}
### `Union` in Python 3.10 { #union-in-python-3-10 } ### `Union` in Python 3.10 { #union-in-python-3-10 }
In this example we pass `Union[PlaneItem, CarItem]` as the value of the argument `response_model`. In this example we pass `Union[PlaneItem, CarItem]` as the value of the argument `response_model`.
@ -204,7 +194,6 @@ For that, use the standard Python `typing.List` (or just `list` in Python 3.9 an
{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *} {* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *}
## Response with arbitrary `dict` { #response-with-arbitrary-dict } ## Response with arbitrary `dict` { #response-with-arbitrary-dict }
You can also declare a response using a plain arbitrary `dict`, declaring just the type of the keys and values, without using a Pydantic model. You can also declare a response using a plain arbitrary `dict`, declaring just the type of the keys and values, without using a Pydantic model.
@ -215,7 +204,6 @@ In this case, you can use `typing.Dict` (or just `dict` in Python 3.9 and above)
{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *} {* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *}
## Recap { #recap } ## Recap { #recap }
Use multiple Pydantic models and inherit freely for each case. Use multiple Pydantic models and inherit freely for each case.

View File

@ -206,20 +206,6 @@ If you feel lost with all these **"regular expression"** ideas, don't worry. The
Now you know that whenever you need them you can use them in **FastAPI**. Now you know that whenever you need them you can use them in **FastAPI**.
### Pydantic v1 `regex` instead of `pattern` { #pydantic-v1-regex-instead-of-pattern }
Before Pydantic version 2 and before FastAPI 0.100.0, the parameter was called `regex` instead of `pattern`, but it's now deprecated.
You could still see some code using it:
//// tab | Pydantic v1
{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *}
////
But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓
## Default values { #default-values } ## Default values { #default-values }
You can, of course, use default values other than `None`. You can, of course, use default values other than `None`.

View File

@ -252,20 +252,6 @@ So, if you send a request to that *path operation* for the item with ID `foo`, t
/// info /// info
In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`.
The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2.
///
/// info
FastAPI uses Pydantic model's `.dict()` with <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">its `exclude_unset` parameter</a> to achieve this.
///
/// info
You can also use: You can also use:
* `response_model_exclude_defaults=True` * `response_model_exclude_defaults=True`

View File

@ -8,36 +8,14 @@ Here are several ways to do it.
You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema. You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema.
//// tab | Pydantic v2
{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} {* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *}
////
//// tab | Pydantic v1
{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *}
////
That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs. That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs.
//// tab | Pydantic v2 You can use the attribute `model_config` that takes a `dict` as described in <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Pydantic's docs: Configuration</a>.
In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Pydantic's docs: Configuration</a>.
You can set `"json_schema_extra"` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. You can set `"json_schema_extra"` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`.
////
//// tab | Pydantic v1
In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>.
You can set `schema_extra` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`.
////
/// tip /// tip
You could use the same technique to extend the JSON Schema and add your own custom extra info. You could use the same technique to extend the JSON Schema and add your own custom extra info.

47
docs/ja/llm-prompt.md Normal file
View File

@ -0,0 +1,47 @@
### Target language
Translate to Japanese (日本語).
Language code: ja.
### Grammar and tone
1) Use polite, instructional Japanese (です/ます調).
2) Keep the tone concise and technical (match existing Japanese FastAPI docs).
### Headings
1) Follow the existing Japanese style: short, descriptive headings (often noun phrases), e.g. 「チェック」.
2) Do not add a trailing period at the end of headings.
### Quotes
1) Prefer Japanese corner brackets 「」 in normal prose when quoting a term.
2) Do not change quotes inside inline code, code blocks, URLs, or file paths.
### Ellipsis
1) Keep ellipsis style consistent with existing Japanese docs (commonly `...`).
2) Never change `...` in code, URLs, or CLI examples.
### Preferred translations / glossary
Use the following preferred translations when they apply in documentation prose:
- request (HTTP): リクエスト
- response (HTTP): レスポンス
- path operation: パスオペレーション
- path operation function: パスオペレーション関数
### `///` admonitions
1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.).
2) If a title is present, prefer these canonical titles:
- `/// note | 備考`
- `/// note | 技術詳細`
- `/// tip | 豆知識`
- `/// warning | 注意`
- `/// info | 情報`
- `/// check | 確認`
- `/// danger | 警告`

51
docs/ko/llm-prompt.md Normal file
View File

@ -0,0 +1,51 @@
### Target language
Translate to Korean (한국어).
Language code: ko.
### Grammar and tone
1) Use polite, instructional Korean (e.g. 합니다/하세요 style).
2) Keep the tone consistent with the existing Korean FastAPI docs.
### Headings
1) Follow existing Korean heading style (short, action-oriented headings like “확인하기”).
2) Do not add trailing punctuation to headings.
### Quotes
1) Keep quote style consistent with the existing Korean docs.
2) Never change quotes inside inline code, code blocks, URLs, or file paths.
### Ellipsis
1) Keep ellipsis style consistent with existing Korean docs (often `...`).
2) Never change `...` in code, URLs, or CLI examples.
### Preferred translations / glossary
Use the following preferred translations when they apply in documentation prose:
- request (HTTP): 요청
- response (HTTP): 응답
- path operation: 경로 처리
- path operation function: 경로 처리 함수
### `///` admonitions
1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.).
2) If a title is present, prefer these canonical titles:
- `/// note | 참고`
- `/// tip | 팁`
- `/// warning | 경고`
- `/// info | 정보`
- `/// danger | 위험`
- `/// note Technical Details | 기술 세부사항`
- `/// check | 확인`
Notes:
- `details` blocks exist in Korean docs; keep `/// details` as-is and translate only the title after `|`.
- Example canonical title used: `/// details | 상세 설명`

46
docs/uk/llm-prompt.md Normal file
View File

@ -0,0 +1,46 @@
### Target language
Translate to Ukrainian (українська).
Language code: uk.
### Grammar and tone
1) Use polite/formal address consistent with existing Ukrainian docs (use “ви/ваш”).
2) Keep the tone concise and technical.
### Headings
1) Follow existing Ukrainian heading style; keep headings short and instructional.
2) Do not add trailing punctuation to headings.
### Quotes
1) Prefer Ukrainian guillemets «…» for quoted terms in prose, matching existing Ukrainian docs.
2) Never change quotes inside inline code, code blocks, URLs, or file paths.
### Ellipsis
1) Keep ellipsis style consistent with existing Ukrainian docs.
2) Never change `...` in code, URLs, or CLI examples.
### Preferred translations / glossary
Use the following preferred translations when they apply in documentation prose:
- request (HTTP): запит
- response (HTTP): відповідь
- path operation: операція шляху
- path operation function: функція операції шляху
### `///` admonitions
1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.).
2) If a title is present, prefer these canonical titles (choose one canonical form where variants exist):
- `/// note | Примітка`
- `/// note | Технічні деталі`
- `/// tip | Порада`
- `/// warning | Попередження`
- `/// info | Інформація`
- `/// danger | Обережно`

View File

@ -14,7 +14,7 @@ app = FastAPI()
@app.post("/items/") @app.post("/items/")
async def create_item(item: Item): async def create_item(item: Item):
item_dict = item.dict() item_dict = item.model_dump()
if item.tax is not None: if item.tax is not None:
price_with_tax = item.price + item.tax price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax}) item_dict.update({"price_with_tax": price_with_tax})

View File

@ -16,7 +16,7 @@ app = FastAPI()
@app.post("/items/") @app.post("/items/")
async def create_item(item: Item): async def create_item(item: Item):
item_dict = item.dict() item_dict = item.model_dump()
if item.tax is not None: if item.tax is not None:
price_with_tax = item.price + item.tax price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax}) item_dict.update({"price_with_tax": price_with_tax})

View File

@ -14,4 +14,4 @@ app = FastAPI()
@app.put("/items/{item_id}") @app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item): async def update_item(item_id: int, item: Item):
return {"item_id": item_id, **item.dict()} return {"item_id": item_id, **item.model_dump()}

View File

@ -16,4 +16,4 @@ app = FastAPI()
@app.put("/items/{item_id}") @app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item): async def update_item(item_id: int, item: Item):
return {"item_id": item_id, **item.dict()} return {"item_id": item_id, **item.model_dump()}

View File

@ -14,7 +14,7 @@ app = FastAPI()
@app.put("/items/{item_id}") @app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, q: str | None = None): async def update_item(item_id: int, item: Item, q: str | None = None):
result = {"item_id": item_id, **item.dict()} result = {"item_id": item_id, **item.model_dump()}
if q: if q:
result.update({"q": q}) result.update({"q": q})
return result return result

View File

@ -16,7 +16,7 @@ app = FastAPI()
@app.put("/items/{item_id}") @app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, q: Union[str, None] = None): async def update_item(item_id: int, item: Item, q: Union[str, None] = None):
result = {"item_id": item_id, **item.dict()} result = {"item_id": item_id, **item.model_dump()}
if q: if q:
result.update({"q": q}) result.update({"q": q})
return result return result

View File

@ -29,7 +29,7 @@ async def read_item(item_id: str):
async def update_item(item_id: str, item: Item): async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id] stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data) stored_item_model = Item(**stored_item_data)
update_data = item.dict(exclude_unset=True) update_data = item.model_dump(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data) updated_item = stored_item_model.model_copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item) items[item_id] = jsonable_encoder(updated_item)
return updated_item return updated_item

View File

@ -31,7 +31,7 @@ async def read_item(item_id: str):
async def update_item(item_id: str, item: Item): async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id] stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data) stored_item_model = Item(**stored_item_data)
update_data = item.dict(exclude_unset=True) update_data = item.model_dump(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data) updated_item = stored_item_model.model_copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item) items[item_id] = jsonable_encoder(updated_item)
return updated_item return updated_item

View File

@ -1,20 +0,0 @@
from typing import Annotated
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

View File

@ -1,20 +0,0 @@
from typing import Annotated, Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies

View File

@ -1,18 +0,0 @@
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies

View File

@ -1,20 +0,0 @@
from typing import Union
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
class Config:
extra = "forbid"
session_id: str
fatebook_tracker: Union[str, None] = None
googall_tracker: Union[str, None] = None
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies

View File

@ -30,7 +30,7 @@ def fake_password_hasher(raw_password: str):
def fake_save_user(user_in: UserIn): def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password) hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
print("User saved! ..not really") print("User saved! ..not really")
return user_in_db return user_in_db

View File

@ -32,7 +32,7 @@ def fake_password_hasher(raw_password: str):
def fake_save_user(user_in: UserIn): def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password) hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
print("User saved! ..not really") print("User saved! ..not really")
return user_in_db return user_in_db

View File

@ -28,7 +28,7 @@ def fake_password_hasher(raw_password: str):
def fake_save_user(user_in: UserIn): def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password) hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
print("User saved! ..not really") print("User saved! ..not really")
return user_in_db return user_in_db

View File

@ -30,7 +30,7 @@ def fake_password_hasher(raw_password: str):
def fake_save_user(user_in: UserIn): def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password) hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
print("User saved! ..not really") print("User saved! ..not really")
return user_in_db return user_in_db

View File

@ -1,22 +0,0 @@
from typing import Annotated
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

View File

@ -1,22 +0,0 @@
from typing import Annotated, Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
return headers

View File

@ -1,20 +0,0 @@
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: str | None = None
traceparent: str | None = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

View File

@ -1,22 +0,0 @@
from typing import Union
from fastapi import FastAPI, Header
from pydantic import BaseModel
app = FastAPI()
class CommonHeaders(BaseModel):
class Config:
extra = "forbid"
host: str
save_data: bool
if_modified_since: Union[str, None] = None
traceparent: Union[str, None] = None
x_tag: list[str] = []
@app.get("/items/")
async def read_items(headers: CommonHeaders = Header()):
return headers

View File

@ -1,6 +1,6 @@
import yaml import yaml
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError from pydantic.v1 import BaseModel, ValidationError
app = FastAPI() app = FastAPI()

View File

@ -1,2 +1,2 @@
def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes): def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
return item_a, item_b, item_c, item_d, item_d, item_e return item_a, item_b, item_c, item_d, item_e

View File

@ -1,21 +0,0 @@
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

View File

@ -1,21 +0,0 @@
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query

View File

@ -1,21 +0,0 @@
from typing import Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

View File

@ -1,21 +0,0 @@
from typing import Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
class Config:
extra = "forbid"
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: FilterParams = Query()):
return filter_query

View File

@ -1,7 +1,8 @@
from typing import Annotated from typing import Annotated
from fastapi import FastAPI, Form from fastapi import FastAPI
from pydantic import BaseModel from fastapi.temp_pydantic_v1_params import Form
from pydantic.v1 import BaseModel
app = FastAPI() app = FastAPI()

View File

@ -1,5 +1,6 @@
from fastapi import FastAPI, Form from fastapi import FastAPI
from pydantic import BaseModel from fastapi.temp_pydantic_v1_params import Form
from pydantic.v1 import BaseModel
app = FastAPI() app = FastAPI()

View File

@ -1,5 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic.v1 import BaseModel
app = FastAPI() app = FastAPI()

View File

@ -1,7 +1,7 @@
from typing import Union from typing import Union
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic.v1 import BaseModel
app = FastAPI() app = FastAPI()

View File

@ -1,4 +1,4 @@
from pydantic import BaseSettings from pydantic.v1 import BaseSettings
class Settings(BaseSettings): class Settings(BaseSettings):

View File

@ -1,4 +1,4 @@
from pydantic import BaseSettings from pydantic.v1 import BaseSettings
class Settings(BaseSettings): class Settings(BaseSettings):

View File

@ -1,5 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseSettings from pydantic.v1 import BaseSettings
class Settings(BaseSettings): class Settings(BaseSettings):

View File

@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production""" """FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.125.0" __version__ = "0.127.0"
from starlette import status as status from starlette import status as status

View File

@ -11,7 +11,6 @@ from .main import _is_model_class as _is_model_class
from .main import _is_model_field as _is_model_field from .main import _is_model_field as _is_model_field
from .main import _is_undefined as _is_undefined from .main import _is_undefined as _is_undefined
from .main import _model_dump as _model_dump 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 copy_field_info as copy_field_info
from .main import create_body_model as create_body_model from .main import create_body_model as create_body_model
from .main import evaluate_forwardref as evaluate_forwardref from .main import evaluate_forwardref as evaluate_forwardref

View File

@ -6,14 +6,13 @@ from typing import (
) )
from fastapi._compat import may_v1 from fastapi._compat import may_v1
from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass from fastapi._compat.shared import lenient_issubclass
from fastapi.types import ModelNameMap from fastapi.types import ModelNameMap
from pydantic import BaseModel from pydantic import BaseModel
from typing_extensions import Literal from typing_extensions import Literal
from . import v2
from .model_field import ModelField from .model_field import ModelField
if PYDANTIC_V2:
from .v2 import BaseConfig as BaseConfig from .v2 import BaseConfig as BaseConfig
from .v2 import FieldInfo as FieldInfo from .v2 import FieldInfo as FieldInfo
from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError from .v2 import PydanticSchemaGenerationError as PydanticSchemaGenerationError
@ -27,22 +26,6 @@ if PYDANTIC_V2:
from .v2 import ( from .v2 import (
with_info_plain_validator_function as with_info_plain_validator_function, 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 @lru_cache
@ -50,7 +33,7 @@ def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]:
if lenient_issubclass(model, may_v1.BaseModel): if lenient_issubclass(model, may_v1.BaseModel):
from fastapi._compat import v1 from fastapi._compat import v1
return v1.get_model_fields(model) return v1.get_model_fields(model) # type: ignore[arg-type,return-value]
else: else:
from . import v2 from . import v2
@ -60,11 +43,8 @@ def get_cached_model_fields(model: type[BaseModel]) -> list[ModelField]:
def _is_undefined(value: object) -> bool: def _is_undefined(value: object) -> bool:
if isinstance(value, may_v1.UndefinedType): if isinstance(value, may_v1.UndefinedType):
return True return True
elif PYDANTIC_V2:
from . import v2
return isinstance(value, v2.UndefinedType) return isinstance(value, v2.UndefinedType)
return False
def _get_model_config(model: BaseModel) -> Any: def _get_model_config(model: BaseModel) -> Any:
@ -72,8 +52,6 @@ def _get_model_config(model: BaseModel) -> Any:
from fastapi._compat import v1 from fastapi._compat import v1
return v1._get_model_config(model) return v1._get_model_config(model)
elif PYDANTIC_V2:
from . import v2
return v2._get_model_config(model) return v2._get_model_config(model)
@ -85,8 +63,6 @@ def _model_dump(
from fastapi._compat import v1 from fastapi._compat import v1
return v1._model_dump(model, mode=mode, **kwargs) return v1._model_dump(model, mode=mode, **kwargs)
elif PYDANTIC_V2:
from . import v2
return v2._model_dump(model, mode=mode, **kwargs) return v2._model_dump(model, mode=mode, **kwargs)
@ -94,11 +70,8 @@ def _model_dump(
def _is_error_wrapper(exc: Exception) -> bool: def _is_error_wrapper(exc: Exception) -> bool:
if isinstance(exc, may_v1.ErrorWrapper): if isinstance(exc, may_v1.ErrorWrapper):
return True return True
elif PYDANTIC_V2:
from . import v2
return isinstance(exc, v2.ErrorWrapper) return isinstance(exc, v2.ErrorWrapper)
return False
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
@ -106,9 +79,6 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.copy_field_info(field_info=field_info, annotation=annotation) 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) return v2.copy_field_info(field_info=field_info, annotation=annotation)
@ -120,9 +90,6 @@ def create_body_model(
from fastapi._compat import v1 from fastapi._compat import v1
return v1.create_body_model(fields=fields, model_name=model_name) 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] return v2.create_body_model(fields=fields, model_name=model_name) # type: ignore[arg-type]
@ -136,9 +103,6 @@ def get_annotation_from_field_info(
return v1.get_annotation_from_field_info( return v1.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name annotation=annotation, field_info=field_info, field_name=field_name
) )
else:
assert PYDANTIC_V2
from . import v2
return v2.get_annotation_from_field_info( return v2.get_annotation_from_field_info(
annotation=annotation, field_info=field_info, field_name=field_name annotation=annotation, field_info=field_info, field_name=field_name
@ -150,9 +114,6 @@ def is_bytes_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_bytes_field(field) return v1.is_bytes_field(field)
else:
assert PYDANTIC_V2
from . import v2
return v2.is_bytes_field(field) # type: ignore[arg-type] return v2.is_bytes_field(field) # type: ignore[arg-type]
@ -162,9 +123,6 @@ def is_bytes_sequence_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_bytes_sequence_field(field) 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] return v2.is_bytes_sequence_field(field) # type: ignore[arg-type]
@ -174,22 +132,11 @@ def is_scalar_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_scalar_field(field) return v1.is_scalar_field(field)
else:
assert PYDANTIC_V2
from . import v2
return v2.is_scalar_field(field) # type: ignore[arg-type] return v2.is_scalar_field(field) # type: ignore[arg-type]
def is_scalar_sequence_field(field: ModelField) -> bool: def is_scalar_sequence_field(field: ModelField) -> bool:
if isinstance(field, may_v1.ModelField):
from fastapi._compat import v1
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] return v2.is_scalar_sequence_field(field) # type: ignore[arg-type]
@ -198,9 +145,6 @@ def is_sequence_field(field: ModelField) -> bool:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.is_sequence_field(field) return v1.is_sequence_field(field)
else:
assert PYDANTIC_V2
from . import v2
return v2.is_sequence_field(field) # type: ignore[arg-type] return v2.is_sequence_field(field) # type: ignore[arg-type]
@ -210,24 +154,10 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
from fastapi._compat import v1 from fastapi._compat import v1
return v1.serialize_sequence_value(field=field, value=value) 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] return v2.serialize_sequence_value(field=field, value=value) # type: ignore[arg-type]
def _model_rebuild(model: type[BaseModel]) -> None:
if lenient_issubclass(model, may_v1.BaseModel):
from fastapi._compat import v1
v1._model_rebuild(model)
elif PYDANTIC_V2:
from . import v2
v2._model_rebuild(model)
def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap: def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap:
v1_model_fields = [ v1_model_fields = [
field for field in fields if isinstance(field, may_v1.ModelField) field for field in fields if isinstance(field, may_v1.ModelField)
@ -236,27 +166,18 @@ def get_compat_model_name_map(fields: list[ModelField]) -> ModelNameMap:
from fastapi._compat import v1 from fastapi._compat import v1
v1_flat_models = v1.get_flat_models_from_fields( v1_flat_models = v1.get_flat_models_from_fields(
v1_model_fields, known_models=set() v1_model_fields, # type: ignore[arg-type]
known_models=set(),
) )
all_flat_models = v1_flat_models all_flat_models = v1_flat_models
else: else:
all_flat_models = set() all_flat_models = set()
if PYDANTIC_V2:
from . import v2
v2_model_fields = [ v2_model_fields = [field for field in fields if isinstance(field, v2.ModelField)]
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) # type: ignore[arg-type]
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) model_name_map = v2.get_model_name_map(all_flat_models) # type: ignore[arg-type]
return model_name_map
from fastapi._compat import v1
model_name_map = v1.get_model_name_map(all_flat_models)
return model_name_map return model_name_map
@ -275,14 +196,10 @@ def get_definitions(
if sys.version_info < (3, 14): if sys.version_info < (3, 14):
v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)] v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)]
v1_field_maps, v1_definitions = may_v1.get_definitions( v1_field_maps, v1_definitions = may_v1.get_definitions(
fields=v1_fields, fields=v1_fields, # type: ignore[arg-type]
model_name_map=model_name_map, model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas, 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_fields = [field for field in fields if isinstance(field, v2.ModelField)]
v2_field_maps, v2_definitions = v2.get_definitions( v2_field_maps, v2_definitions = v2.get_definitions(
@ -291,13 +208,11 @@ def get_definitions(
separate_input_output_schemas=separate_input_output_schemas, separate_input_output_schemas=separate_input_output_schemas,
) )
all_definitions = {**v1_definitions, **v2_definitions} all_definitions = {**v1_definitions, **v2_definitions}
all_field_maps = {**v1_field_maps, **v2_field_maps} all_field_maps = {**v1_field_maps, **v2_field_maps} # type: ignore[misc]
return all_field_maps, all_definitions return all_field_maps, all_definitions
# Pydantic v1 is not supported since Python 3.14 # Pydantic v1 is not supported since Python 3.14
else: else:
from . import v2
v2_fields = [field for field in fields if isinstance(field, v2.ModelField)] v2_fields = [field for field in fields if isinstance(field, v2.ModelField)]
v2_field_maps, v2_definitions = v2.get_definitions( v2_field_maps, v2_definitions = v2.get_definitions(
fields=v2_fields, fields=v2_fields,
@ -326,9 +241,6 @@ def get_schema_from_model_field(
field_mapping=field_mapping, field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas, separate_input_output_schemas=separate_input_output_schemas,
) )
else:
assert PYDANTIC_V2
from . import v2
return v2.get_schema_from_model_field( return v2.get_schema_from_model_field(
field=field, # type: ignore[arg-type] field=field, # type: ignore[arg-type]
@ -341,18 +253,12 @@ def get_schema_from_model_field(
def _is_model_field(value: Any) -> bool: def _is_model_field(value: Any) -> bool:
if isinstance(value, may_v1.ModelField): if isinstance(value, may_v1.ModelField):
return True return True
elif PYDANTIC_V2:
from . import v2
return isinstance(value, v2.ModelField) return isinstance(value, v2.ModelField)
return False
def _is_model_class(value: Any) -> bool: def _is_model_class(value: Any) -> bool:
if lenient_issubclass(value, may_v1.BaseModel): if lenient_issubclass(value, may_v1.BaseModel):
return True return True
elif PYDANTIC_V2:
from . import v2
return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined] return lenient_issubclass(value, v2.BaseModel) # type: ignore[attr-defined]
return False

View File

@ -102,7 +102,7 @@ def _normalize_errors(errors: Sequence[Any]) -> list[dict[str, Any]]:
use_errors: list[Any] = [] use_errors: list[Any] = []
for error in errors: for error in errors:
if isinstance(error, ErrorWrapper): if isinstance(error, ErrorWrapper):
new_errors = ValidationError( # type: ignore[call-arg] new_errors = ValidationError(
errors=[error], model=RequestErrorModel errors=[error], model=RequestErrorModel
).errors() ).errors()
use_errors.extend(new_errors) use_errors.extend(new_errors)

View File

@ -11,71 +11,13 @@ from typing import (
from fastapi._compat import shared from fastapi._compat import shared
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
from fastapi.types import ModelNameMap from fastapi.types import ModelNameMap
from pydantic.version import VERSION as PYDANTIC_VERSION from pydantic.v1 import BaseConfig as BaseConfig
from typing_extensions import Literal from pydantic.v1 import BaseModel as BaseModel
from pydantic.v1 import ValidationError as ValidationError
PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2]) from pydantic.v1 import create_model as create_model
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,
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_flat_models_from_fields as get_flat_models_from_fields,
)
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.class_validators import Validator as Validator
from pydantic.v1.color import Color as Color # type: ignore[assignment] from pydantic.v1.color import Color as Color
from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper from pydantic.v1.error_wrappers import ErrorWrapper as ErrorWrapper
from pydantic.v1.errors import MissingError
from pydantic.v1.fields import ( from pydantic.v1.fields import (
SHAPE_FROZENSET, SHAPE_FROZENSET,
SHAPE_LIST, SHAPE_LIST,
@ -85,19 +27,14 @@ else:
SHAPE_TUPLE, SHAPE_TUPLE,
SHAPE_TUPLE_ELLIPSIS, SHAPE_TUPLE_ELLIPSIS,
) )
from pydantic.v1.fields import FieldInfo as FieldInfo # type: ignore[assignment] from pydantic.v1.fields import FieldInfo as FieldInfo
from pydantic.v1.fields import ModelField as ModelField from pydantic.v1.fields import ModelField as ModelField
from pydantic.v1.fields import Undefined as Undefined from pydantic.v1.fields import Undefined as Undefined
from pydantic.v1.fields import UndefinedType as UndefinedType from pydantic.v1.fields import UndefinedType as UndefinedType
from pydantic.v1.networks import AnyUrl as AnyUrl from pydantic.v1.networks import AnyUrl as AnyUrl
from pydantic.v1.networks import ( # type: ignore[assignment] from pydantic.v1.networks import NameEmail as NameEmail
NameEmail as NameEmail,
)
from pydantic.v1.schema import TypeModelSet as TypeModelSet from pydantic.v1.schema import TypeModelSet as TypeModelSet
from pydantic.v1.schema import ( from pydantic.v1.schema import field_schema, model_process_schema
field_schema,
model_process_schema,
)
from pydantic.v1.schema import ( from pydantic.v1.schema import (
get_annotation_from_field_info as get_annotation_from_field_info, get_annotation_from_field_info as get_annotation_from_field_info,
) )
@ -108,14 +45,18 @@ else:
get_flat_models_from_fields as get_flat_models_from_fields, get_flat_models_from_fields as get_flat_models_from_fields,
) )
from pydantic.v1.schema import get_model_name_map as get_model_name_map from pydantic.v1.schema import get_model_name_map as get_model_name_map
from pydantic.v1.types import ( # type: ignore[assignment] from pydantic.v1.types import SecretBytes as SecretBytes
SecretBytes as SecretBytes, from pydantic.v1.types import SecretStr as SecretStr
)
from pydantic.v1.types import ( # type: ignore[assignment]
SecretStr as SecretStr,
)
from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref from pydantic.v1.typing import evaluate_forwardref as evaluate_forwardref
from pydantic.v1.utils import lenient_issubclass as lenient_issubclass from pydantic.v1.utils import lenient_issubclass as lenient_issubclass
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
GetJsonSchemaHandler = Any GetJsonSchemaHandler = Any
@ -200,24 +141,6 @@ def is_pv1_scalar_field(field: ModelField) -> bool:
return True 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 _model_rebuild(model: type[BaseModel]) -> None:
model.update_forward_refs()
def _model_dump( def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any: ) -> Any:
@ -225,7 +148,7 @@ def _model_dump(
def _get_model_config(model: BaseModel) -> Any: def _get_model_config(model: BaseModel) -> Any:
return model.__config__ # type: ignore[attr-defined] return model.__config__
def get_schema_from_model_field( def get_schema_from_model_field(
@ -237,8 +160,10 @@ def get_schema_from_model_field(
], ],
separate_input_output_schemas: bool = True, separate_input_output_schemas: bool = True,
) -> dict[str, Any]: ) -> dict[str, Any]:
return field_schema( # type: ignore[no-any-return] return field_schema(
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX field,
model_name_map=model_name_map, # type: ignore[arg-type]
ref_prefix=REF_PREFIX,
)[0] )[0]
@ -257,7 +182,7 @@ def get_definitions(
dict[str, dict[str, Any]], dict[str, dict[str, Any]],
]: ]:
models = get_flat_models_from_fields(fields, known_models=set()) models = get_flat_models_from_fields(fields, known_models=set())
return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) return {}, get_model_definitions(flat_models=models, model_name_map=model_name_map) # type: ignore[arg-type]
def is_scalar_field(field: ModelField) -> bool: def is_scalar_field(field: ModelField) -> bool:
@ -268,12 +193,8 @@ def is_sequence_field(field: ModelField) -> bool:
return field.shape in sequence_shapes or shared._annotation_is_sequence(field.type_) 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: def is_bytes_field(field: ModelField) -> bool:
return lenient_issubclass(field.type_, bytes) # type: ignore[no-any-return] return lenient_issubclass(field.type_, bytes)
def is_bytes_sequence_field(field: ModelField) -> bool: def is_bytes_sequence_field(field: ModelField) -> bool:
@ -288,20 +209,14 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return] 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( def create_body_model(
*, fields: Sequence[ModelField], model_name: str *, fields: Sequence[ModelField], model_name: str
) -> type[BaseModel]: ) -> type[BaseModel]:
BodyModel = create_model(model_name) BodyModel = create_model(model_name)
for f in fields: for f in fields:
BodyModel.__fields__[f.name] = f # type: ignore[index] BodyModel.__fields__[f.name] = f
return BodyModel return BodyModel
def get_model_fields(model: type[BaseModel]) -> list[ModelField]: def get_model_fields(model: type[BaseModel]) -> list[ModelField]:
return list(model.__fields__.values()) # type: ignore[attr-defined] return list(model.__fields__.values())

View File

@ -216,10 +216,6 @@ def get_annotation_from_field_info(
return annotation return annotation
def _model_rebuild(model: type[BaseModel]) -> None:
model.model_rebuild()
def _model_dump( def _model_dump(
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
) -> Any: ) -> Any:

View File

@ -1,4 +1,3 @@
from collections.abc import Iterable
from typing import ( from typing import (
Annotated, Annotated,
Any, Any,
@ -135,27 +134,12 @@ class UploadFile(StarletteUploadFile):
""" """
return await super().close() return await super().close()
@classmethod
def __get_validators__(cls: type["UploadFile"]) -> Iterable[Callable[..., Any]]:
yield cls.validate
@classmethod
def validate(cls: type["UploadFile"], v: Any) -> Any:
if not isinstance(v, StarletteUploadFile):
raise ValueError(f"Expected UploadFile, received: {type(v)}")
return v
@classmethod @classmethod
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile": def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
if not isinstance(__input_value, StarletteUploadFile): if not isinstance(__input_value, StarletteUploadFile):
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}") raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
return cast(UploadFile, __input_value) return cast(UploadFile, __input_value)
# 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 @classmethod
def __get_pydantic_json_schema__( def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler

View File

@ -1,6 +1,7 @@
import dataclasses import dataclasses
import inspect import inspect
import sys import sys
import warnings
from collections.abc import Coroutine, Mapping, Sequence from collections.abc import Coroutine, Mapping, Sequence
from contextlib import AsyncExitStack, contextmanager from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy from copy import copy, deepcopy
@ -18,7 +19,6 @@ from typing import (
import anyio import anyio
from fastapi import params from fastapi import params
from fastapi._compat import ( from fastapi._compat import (
PYDANTIC_V2,
ModelField, ModelField,
RequiredParam, RequiredParam,
Undefined, Undefined,
@ -323,6 +323,13 @@ def get_dependant(
) )
continue continue
assert param_details.field is not None assert param_details.field is not None
if isinstance(param_details.field, may_v1.ModelField):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" Please update the param {param_name}: {param_details.type_annotation!r}.",
category=DeprecationWarning,
stacklevel=5,
)
if isinstance( if isinstance(
param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body) param_details.field.field_info, (params.Body, temp_pydantic_v1_params.Body)
): ):
@ -410,7 +417,8 @@ def analyze_param(
if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)): if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)):
# Copy `field_info` because we mutate `field_info.default` below. # Copy `field_info` because we mutate `field_info.default` below.
field_info = copy_field_info( field_info = copy_field_info(
field_info=fastapi_annotation, annotation=use_annotation field_info=fastapi_annotation, # type: ignore[arg-type]
annotation=use_annotation,
) )
assert field_info.default in { assert field_info.default in {
Undefined, Undefined,
@ -444,8 +452,7 @@ def analyze_param(
"Cannot specify FastAPI annotations in `Annotated` and default value" "Cannot specify FastAPI annotations in `Annotated` and default value"
f" together for {param_name!r}" f" together for {param_name!r}"
) )
field_info = value field_info = value # type: ignore[assignment]
if PYDANTIC_V2:
if isinstance(field_info, FieldInfo): if isinstance(field_info, FieldInfo):
field_info.annotation = type_annotation field_info.annotation = type_annotation
@ -485,7 +492,7 @@ def analyze_param(
field_info = params.File(annotation=use_annotation, default=default_value) field_info = params.File(annotation=use_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation): elif not field_annotation_is_scalar(annotation=type_annotation):
if annotation_is_pydantic_v1(use_annotation): if annotation_is_pydantic_v1(use_annotation):
field_info = temp_pydantic_v1_params.Body( field_info = temp_pydantic_v1_params.Body( # type: ignore[assignment]
annotation=use_annotation, default=default_value annotation=use_annotation, default=default_value
) )
else: else:

View File

@ -228,11 +228,11 @@ def jsonable_encoder(
# TODO: remove when deprecating Pydantic v1 # TODO: remove when deprecating Pydantic v1
encoders: dict[Any, Any] = {} encoders: dict[Any, Any] = {}
if isinstance(obj, may_v1.BaseModel): if isinstance(obj, may_v1.BaseModel):
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] encoders = getattr(obj.__config__, "json_encoders", {})
if custom_encoder: if custom_encoder:
encoders = {**encoders, **custom_encoder} encoders = {**encoders, **custom_encoder}
obj_dict = _model_dump( obj_dict = _model_dump(
obj, obj, # type: ignore[arg-type]
mode="json", mode="json",
include=include, include=include,
exclude=exclude, exclude=exclude,

View File

@ -3,11 +3,9 @@ from enum import Enum
from typing import Annotated, Any, Callable, Optional, Union from typing import Annotated, Any, Callable, Optional, Union
from fastapi._compat import ( from fastapi._compat import (
PYDANTIC_V2,
CoreSchema, CoreSchema,
GetJsonSchemaHandler, GetJsonSchemaHandler,
JsonSchemaValue, JsonSchemaValue,
_model_rebuild,
with_info_plain_validator_function, with_info_plain_validator_function,
) )
from fastapi.logger import logger from fastapi.logger import logger
@ -57,14 +55,8 @@ except ImportError: # pragma: no cover
class BaseModelWithConfig(BaseModel): class BaseModelWithConfig(BaseModel):
if PYDANTIC_V2:
model_config = {"extra": "allow"} model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
class Contact(BaseModelWithConfig): class Contact(BaseModelWithConfig):
name: Optional[str] = None name: Optional[str] = None
@ -226,13 +218,7 @@ class Example(TypedDict, total=False):
value: Optional[Any] value: Optional[Any]
externalValue: Optional[AnyUrl] externalValue: Optional[AnyUrl]
if PYDANTIC_V2: # type: ignore [misc] __pydantic_config__ = {"extra": "allow"} # type: ignore[misc]
__pydantic_config__ = {"extra": "allow"}
else:
class Config:
extra = "allow"
class ParameterInType(Enum): class ParameterInType(Enum):
@ -447,6 +433,6 @@ class OpenAPI(BaseModelWithConfig):
externalDocs: Optional[ExternalDocumentation] = None externalDocs: Optional[ExternalDocumentation] = None
_model_rebuild(Schema) Schema.model_rebuild()
_model_rebuild(Operation) Operation.model_rebuild()
_model_rebuild(Encoding) Encoding.model_rebuild()

View File

@ -9,8 +9,6 @@ from pydantic.fields import FieldInfo
from typing_extensions import Literal, deprecated from typing_extensions import Literal, deprecated
from ._compat import ( from ._compat import (
PYDANTIC_V2,
PYDANTIC_VERSION_MINOR_TUPLE,
Undefined, Undefined,
) )
@ -111,11 +109,8 @@ class Param(FieldInfo): # type: ignore[misc]
stacklevel=4, stacklevel=4,
) )
current_json_schema_extra = json_schema_extra or extra current_json_schema_extra = json_schema_extra or extra
if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
self.deprecated = deprecated
else:
kwargs["deprecated"] = deprecated kwargs["deprecated"] = deprecated
if PYDANTIC_V2:
if serialization_alias in (_Unset, None) and isinstance(alias, str): if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias serialization_alias = alias
if validation_alias in (_Unset, None): if validation_alias in (_Unset, None):
@ -131,9 +126,7 @@ class Param(FieldInfo): # type: ignore[misc]
} }
) )
kwargs["pattern"] = pattern or regex kwargs["pattern"] = pattern or regex
else:
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} use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
super().__init__(**use_kwargs) super().__init__(**use_kwargs)
@ -571,11 +564,7 @@ class Body(FieldInfo): # type: ignore[misc]
stacklevel=4, stacklevel=4,
) )
current_json_schema_extra = json_schema_extra or extra current_json_schema_extra = json_schema_extra or extra
if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
self.deprecated = deprecated
else:
kwargs["deprecated"] = deprecated kwargs["deprecated"] = deprecated
if PYDANTIC_V2:
if serialization_alias in (_Unset, None) and isinstance(alias, str): if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias serialization_alias = alias
if validation_alias in (_Unset, None): if validation_alias in (_Unset, None):
@ -591,9 +580,6 @@ class Body(FieldInfo): # type: ignore[misc]
} }
) )
kwargs["pattern"] = pattern or regex kwargs["pattern"] = pattern or regex
else:
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} use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}

View File

@ -1,8 +1,8 @@
import dataclasses
import email.message import email.message
import functools import functools
import inspect import inspect
import json import json
import warnings
from collections.abc import ( from collections.abc import (
AsyncIterator, AsyncIterator,
Awaitable, Awaitable,
@ -30,7 +30,9 @@ from fastapi._compat import (
_get_model_config, _get_model_config,
_model_dump, _model_dump,
_normalize_errors, _normalize_errors,
annotation_is_pydantic_v1,
lenient_issubclass, lenient_issubclass,
may_v1,
) )
from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant
@ -59,7 +61,6 @@ from fastapi.utils import (
get_value_or_default, get_value_or_default,
is_body_allowed_for_status_code, is_body_allowed_for_status_code,
) )
from pydantic import BaseModel
from starlette import routing from starlette import routing
from starlette._exception_handler import wrap_app_handling_exceptions from starlette._exception_handler import wrap_app_handling_exceptions
from starlette._utils import is_async_callable from starlette._utils import is_async_callable
@ -154,8 +155,8 @@ def _prepare_response_content(
exclude_defaults: bool = False, exclude_defaults: bool = False,
exclude_none: bool = False, exclude_none: bool = False,
) -> Any: ) -> Any:
if isinstance(res, BaseModel): if isinstance(res, may_v1.BaseModel):
read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) # type: ignore[arg-type]
if read_with_orm_mode: if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting # Let from_orm extract the data from this model instead of converting
# it now to a dict. # it now to a dict.
@ -163,7 +164,7 @@ def _prepare_response_content(
# access instead of dict iteration, e.g. lazy relationships. # access instead of dict iteration, e.g. lazy relationships.
return res return res
return _model_dump( return _model_dump(
res, res, # type: ignore[arg-type]
by_alias=True, by_alias=True,
exclude_unset=exclude_unset, exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults, exclude_defaults=exclude_defaults,
@ -189,9 +190,6 @@ def _prepare_response_content(
) )
for k, v in res.items() for k, v in res.items()
} }
elif dataclasses.is_dataclass(res):
assert not isinstance(res, type)
return dataclasses.asdict(res)
return res return res
@ -639,6 +637,14 @@ class APIRoute(routing.Route):
assert is_body_allowed_for_status_code(status_code), ( assert is_body_allowed_for_status_code(status_code), (
f"Status code {status_code} must not have a response body" f"Status code {status_code} must not have a response body"
) )
if annotation_is_pydantic_v1(self.response_model):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" Please update the response model {self.response_model!r}.",
category=DeprecationWarning,
stacklevel=4,
)
self.dependencies = list(dependencies or []) self.dependencies = list(dependencies or [])
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
# if a "form feed" character (page break) is found in the description text, # if a "form feed" character (page break) is found in the description text,
@ -684,6 +690,13 @@ class APIRoute(routing.Route):
f"Status code {additional_status_code} must not have a response body" f"Status code {additional_status_code} must not have a response body"
) )
response_name = f"Response_{additional_status_code}_{self.unique_id}" response_name = f"Response_{additional_status_code}_{self.unique_id}"
if annotation_is_pydantic_v1(model):
warnings.warn(
"pydantic.v1 is deprecated and will soon stop being supported by FastAPI."
f" In responses={{}}, please update {model}.",
category=DeprecationWarning,
stacklevel=4,
)
response_field = create_model_field( response_field = create_model_field(
name=response_name, type_=model, mode="serialization" name=response_name, type_=model, mode="serialization"
) )

View File

@ -6,12 +6,11 @@ from fastapi.params import ParamTypes
from typing_extensions import deprecated from typing_extensions import deprecated
from ._compat.may_v1 import FieldInfo, Undefined from ._compat.may_v1 import FieldInfo, Undefined
from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE
_Unset: Any = Undefined _Unset: Any = Undefined
class Param(FieldInfo): # type: ignore[misc] class Param(FieldInfo):
in_: ParamTypes in_: ParamTypes
def __init__( def __init__(
@ -98,9 +97,6 @@ class Param(FieldInfo): # type: ignore[misc]
stacklevel=4, stacklevel=4,
) )
current_json_schema_extra = json_schema_extra or extra current_json_schema_extra = json_schema_extra or extra
if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
self.deprecated = deprecated
else:
kwargs["deprecated"] = deprecated kwargs["deprecated"] = deprecated
kwargs["regex"] = pattern or regex kwargs["regex"] = pattern or regex
kwargs.update(**current_json_schema_extra) kwargs.update(**current_json_schema_extra)
@ -112,7 +108,7 @@ class Param(FieldInfo): # type: ignore[misc]
return f"{self.__class__.__name__}({self.default})" return f"{self.__class__.__name__}({self.default})"
class Path(Param): # type: ignore[misc] class Path(Param):
in_ = ParamTypes.path in_ = ParamTypes.path
def __init__( def __init__(
@ -198,7 +194,7 @@ class Path(Param): # type: ignore[misc]
) )
class Query(Param): # type: ignore[misc] class Query(Param):
in_ = ParamTypes.query in_ = ParamTypes.query
def __init__( def __init__(
@ -282,7 +278,7 @@ class Query(Param): # type: ignore[misc]
) )
class Header(Param): # type: ignore[misc] class Header(Param):
in_ = ParamTypes.header in_ = ParamTypes.header
def __init__( def __init__(
@ -368,7 +364,7 @@ class Header(Param): # type: ignore[misc]
) )
class Cookie(Param): # type: ignore[misc] class Cookie(Param):
in_ = ParamTypes.cookie in_ = ParamTypes.cookie
def __init__( def __init__(
@ -452,7 +448,7 @@ class Cookie(Param): # type: ignore[misc]
) )
class Body(FieldInfo): # type: ignore[misc] class Body(FieldInfo):
def __init__( def __init__(
self, self,
default: Any = Undefined, default: Any = Undefined,
@ -541,9 +537,6 @@ class Body(FieldInfo): # type: ignore[misc]
stacklevel=4, stacklevel=4,
) )
current_json_schema_extra = json_schema_extra or extra current_json_schema_extra = json_schema_extra or extra
if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
self.deprecated = deprecated
else:
kwargs["deprecated"] = deprecated kwargs["deprecated"] = deprecated
kwargs["regex"] = pattern or regex kwargs["regex"] = pattern or regex
kwargs.update(**current_json_schema_extra) kwargs.update(**current_json_schema_extra)
@ -556,7 +549,7 @@ class Body(FieldInfo): # type: ignore[misc]
return f"{self.__class__.__name__}({self.default})" return f"{self.__class__.__name__}({self.default})"
class Form(Body): # type: ignore[misc] class Form(Body):
def __init__( def __init__(
self, self,
default: Any = Undefined, default: Any = Undefined,
@ -640,7 +633,7 @@ class Form(Body): # type: ignore[misc]
) )
class File(Form): # type: ignore[misc] class File(Form):
def __init__( def __init__(
self, self,
default: Any = Undefined, default: Any = Undefined,

View File

@ -1,7 +1,6 @@
import re import re
import warnings import warnings
from collections.abc import MutableMapping from collections.abc import MutableMapping
from dataclasses import is_dataclass
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@ -13,7 +12,6 @@ from weakref import WeakKeyDictionary
import fastapi import fastapi
from fastapi._compat import ( from fastapi._compat import (
PYDANTIC_V2,
BaseConfig, BaseConfig,
ModelField, ModelField,
PydanticSchemaGenerationError, PydanticSchemaGenerationError,
@ -29,6 +27,8 @@ from pydantic import BaseModel
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from typing_extensions import Literal from typing_extensions import Literal
from ._compat import v2
if TYPE_CHECKING: # pragma: nocover if TYPE_CHECKING: # pragma: nocover
from .routing import APIRoute from .routing import APIRoute
@ -105,14 +105,12 @@ def create_model_field(
from fastapi._compat import v1 from fastapi._compat import v1
try: try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] return v1.ModelField(**v1_kwargs) # type: ignore[return-value]
except RuntimeError: except RuntimeError:
raise fastapi.exceptions.FastAPIError( raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_) _invalid_args_message.format(type_=type_)
) from None ) from None
elif PYDANTIC_V2: else:
from ._compat import v2
field_info = field_info or FieldInfo( field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias annotation=type_, default=default, alias=alias
) )
@ -128,7 +126,7 @@ def create_model_field(
from fastapi._compat import v1 from fastapi._compat import v1
try: try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] return v1.ModelField(**v1_kwargs)
except RuntimeError: except RuntimeError:
raise fastapi.exceptions.FastAPIError( raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_) _invalid_args_message.format(type_=type_)
@ -140,9 +138,6 @@ def create_cloned_field(
*, *,
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None, cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
) -> ModelField: ) -> ModelField:
if PYDANTIC_V2:
from ._compat import v2
if isinstance(field, v2.ModelField): if isinstance(field, v2.ModelField):
return field return field
@ -154,8 +149,6 @@ def create_cloned_field(
cloned_types = _CLONED_TYPES_CACHE cloned_types = _CLONED_TYPES_CACHE
original_type = field.type_ original_type = field.type_
if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
original_type = original_type.__pydantic_model__
use_type = original_type use_type = original_type
if lenient_issubclass(original_type, v1.BaseModel): if lenient_issubclass(original_type, v1.BaseModel):
original_type = cast(type[v1.BaseModel], original_type) original_type = cast(type[v1.BaseModel], original_type)

View File

@ -30,7 +30,6 @@ classifiers = [
"Framework :: AsyncIO", "Framework :: AsyncIO",
"Framework :: FastAPI", "Framework :: FastAPI",
"Framework :: Pydantic", "Framework :: Pydantic",
"Framework :: Pydantic :: 1",
"Framework :: Pydantic :: 2", "Framework :: Pydantic :: 2",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
@ -45,7 +44,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"starlette>=0.40.0,<0.51.0", "starlette>=0.40.0,<0.51.0",
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "pydantic>=2.7.0",
"typing-extensions>=4.8.0", "typing-extensions>=4.8.0",
"annotated-doc>=0.0.2", "annotated-doc>=0.0.2",
] ]
@ -71,11 +70,10 @@ standard = [
"email-validator >=2.0.0", "email-validator >=2.0.0",
# Uvicorn with uvloop # Uvicorn with uvloop
"uvicorn[standard] >=0.12.0", "uvicorn[standard] >=0.12.0",
# TODO: this should be part of some pydantic optional extra dependencies
# # Settings management # # Settings management
# "pydantic-settings >=2.0.0", "pydantic-settings >=2.0.0",
# # Extra Pydantic data types # # Extra Pydantic data types
# "pydantic-extra-types >=2.0.0", "pydantic-extra-types >=2.0.0",
] ]
standard-no-fastapi-cloud-cli = [ standard-no-fastapi-cloud-cli = [
@ -90,11 +88,10 @@ standard-no-fastapi-cloud-cli = [
"email-validator >=2.0.0", "email-validator >=2.0.0",
# Uvicorn with uvloop # Uvicorn with uvloop
"uvicorn[standard] >=0.12.0", "uvicorn[standard] >=0.12.0",
# TODO: this should be part of some pydantic optional extra dependencies
# # Settings management # # Settings management
# "pydantic-settings >=2.0.0", "pydantic-settings >=2.0.0",
# # Extra Pydantic data types # # Extra Pydantic data types
# "pydantic-extra-types >=2.0.0", "pydantic-extra-types >=2.0.0",
] ]
all = [ all = [
@ -183,8 +180,6 @@ filterwarnings = [
# Ref: https://github.com/python-trio/trio/pull/3054 # Ref: https://github.com/python-trio/trio/pull/3054
# Remove once there's a new version of Trio # Remove once there's a new version of Trio
'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio', 'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio',
# Ignore flaky coverage / pytest warning about SQLite connection, only applies to Python 3.13 and Pydantic v1
'ignore:Exception ignored in. <sqlite3\.Connection object.*:pytest.PytestUnraisableExceptionWarning',
] ]
[tool.coverage.run] [tool.coverage.run]

View File

@ -2,6 +2,6 @@
-r requirements-tests.txt -r requirements-tests.txt
-r requirements-docs.txt -r requirements-docs.txt
-r requirements-translations.txt -r requirements-translations.txt
pre-commit >=4.5.0,<5.0.0 prek==0.2.22
# For generating screenshots # For generating screenshots
playwright playwright

View File

@ -19,7 +19,13 @@ from slugify import slugify as py_slugify
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
SUPPORTED_LANGS = {"en", "de", "es", "pt", "ru"} SUPPORTED_LANGS = {
"en",
"de",
"es",
"pt",
"ru",
}
app = typer.Typer() app = typer.Typer()
@ -232,27 +238,15 @@ def generate_readme() -> None:
""" """
Generate README.md content from main index.md Generate README.md content from main index.md
""" """
typer.echo("Generating README")
readme_path = Path("README.md") readme_path = Path("README.md")
old_content = readme_path.read_text()
new_content = generate_readme_content() new_content = generate_readme_content()
if new_content != old_content:
print("README.md outdated from the latest index.md")
print("Updating README.md")
readme_path.write_text(new_content, encoding="utf-8") readme_path.write_text(new_content, encoding="utf-8")
raise typer.Exit(1)
print("README.md is up to date ✅")
@app.command()
def verify_readme() -> None:
"""
Verify README.md content from main index.md
"""
typer.echo("Verifying README")
readme_path = Path("README.md")
generated_content = generate_readme_content()
readme_content = readme_path.read_text("utf-8")
if generated_content != readme_content:
typer.secho(
"README.md outdated from the latest index.md", color=typer.colors.RED
)
raise typer.Abort()
typer.echo("Valid README ✅")
@app.command() @app.command()
@ -280,7 +274,17 @@ def update_languages() -> None:
""" """
Update the mkdocs.yml file Languages section including all the available languages. Update the mkdocs.yml file Languages section including all the available languages.
""" """
update_config() old_config = get_en_config()
updated_config = get_updated_config_content()
if old_config != updated_config:
print("docs/en/mkdocs.yml outdated")
print("Updating docs/en/mkdocs.yml")
en_config_path.write_text(
yaml.dump(updated_config, sort_keys=False, width=200, allow_unicode=True),
encoding="utf-8",
)
raise typer.Exit(1)
print("docs/en/mkdocs.yml is up to date ✅")
@app.command() @app.command()
@ -367,39 +371,12 @@ def get_updated_config_content() -> dict[str, Any]:
return config return config
def update_config() -> None:
config = get_updated_config_content()
en_config_path.write_text(
yaml.dump(config, sort_keys=False, width=200, allow_unicode=True),
encoding="utf-8",
)
@app.command() @app.command()
def verify_config() -> None: def ensure_non_translated() -> None:
""" """
Verify main mkdocs.yml content to make sure it uses the latest language names. Ensure there are no files in the non translatable pages.
""" """
typer.echo("Verifying mkdocs.yml") print("Ensuring no non translated pages")
config = get_en_config()
updated_config = get_updated_config_content()
if config != updated_config:
typer.secho(
"docs/en/mkdocs.yml outdated from docs/language_names.yml, "
"update language_names.yml and run "
"python ./scripts/docs.py update-languages",
color=typer.colors.RED,
)
raise typer.Abort()
typer.echo("Valid mkdocs.yml ✅")
@app.command()
def verify_non_translated() -> None:
"""
Verify there are no files in the non translatable pages.
"""
print("Verifying non translated pages")
lang_paths = get_lang_paths() lang_paths = get_lang_paths()
error_paths = [] error_paths = []
for lang in lang_paths: for lang in lang_paths:
@ -410,20 +387,17 @@ def verify_non_translated() -> None:
if non_translatable_path.exists(): if non_translatable_path.exists():
error_paths.append(non_translatable_path) error_paths.append(non_translatable_path)
if error_paths: if error_paths:
print("Non-translated pages found, remove them:") print("Non-translated pages found, removing them:")
for error_path in error_paths: for error_path in error_paths:
print(error_path) print(error_path)
raise typer.Abort() if error_path.is_file():
error_path.unlink()
else:
shutil.rmtree(error_path)
raise typer.Exit(1)
print("No non-translated pages found ✅") print("No non-translated pages found ✅")
@app.command()
def verify_docs():
verify_readme()
verify_config()
verify_non_translated()
@app.command() @app.command()
def langs_json(): def langs_json():
langs = [] langs = []

View File

@ -727,7 +727,7 @@ def translate_page(
print(f"Found existing translation: {out_path}") print(f"Found existing translation: {out_path}")
old_translation = out_path.read_text(encoding="utf-8") old_translation = out_path.read_text(encoding="utf-8")
print(f"Translating {en_path} to {language} ({language_name})") print(f"Translating {en_path} to {language} ({language_name})")
agent = Agent("openai:gpt-5") agent = Agent("openai:gpt-5.2")
prompt_segments = [ prompt_segments = [
general_prompt, general_prompt,

View File

@ -1,5 +1,6 @@
import json import json
import sys import sys
import warnings
from collections.abc import Iterator from collections.abc import Iterator
from typing import Annotated, Any from typing import Annotated, Any
@ -84,6 +85,13 @@ def app(basemodel_class: type[Any]) -> FastAPI:
app = FastAPI() app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.filterwarnings(
"ignore",
message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
category=DeprecationWarning,
)
@app.post("/sync/validated", response_model=ItemOut) @app.post("/sync/validated", response_model=ItemOut)
def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]):
return ItemOut(name=item.name, value=item.value, dep=dep) return ItemOut(name=item.name, value=item.value, dep=dep)

View File

@ -2,18 +2,12 @@ from typing import Union
from dirty_equals import IsDict from dirty_equals import IsDict
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
class FooBaseModel(BaseModel): class FooBaseModel(BaseModel):
if PYDANTIC_V2:
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")
else:
class Config:
extra = "forbid"
class Foo(FooBaseModel): class Foo(FooBaseModel):

View File

@ -4,7 +4,6 @@ import pytest
from fastapi import Depends, FastAPI, Path from fastapi import Depends, FastAPI, Path
from fastapi.param_functions import Query from fastapi.param_functions import Query
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from fastapi.utils import PYDANTIC_V2
app = FastAPI() app = FastAPI()
@ -71,6 +70,5 @@ def test_multiple_annotations():
response = client.get("/multi-query", params={"foo": "123"}) response = client.get("/multi-query", params={"foo": "123"})
assert response.status_code == 422 assert response.status_code == 422
if PYDANTIC_V2:
response = client.get("/multi-query", params={"foo": "1"}) response = client.get("/multi-query", params={"foo": "1"})
assert response.status_code == 422 assert response.status_code == 422

View File

@ -5,8 +5,6 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from inline_snapshot import snapshot from inline_snapshot import snapshot
from .utils import needs_pydanticv2
@pytest.fixture(name="client") @pytest.fixture(name="client")
def get_client(): def get_client():
@ -42,13 +40,11 @@ def get_client():
return client return client
@needs_pydanticv2
def test_get(client: TestClient): def test_get(client: TestClient):
response = client.get("/") response = client.get("/")
assert response.json() == {"custom_field": [1.0, 2.0, 3.0]} assert response.json() == {"custom_field": [1.0, 2.0, 3.0]}
@needs_pydanticv2
def test_typeadapter(): def test_typeadapter():
# This test is only to confirm that Pydantic alone is working as expected # This test is only to confirm that Pydantic alone is working as expected
from pydantic import ( from pydantic import (
@ -93,7 +89,6 @@ def test_typeadapter():
) )
@needs_pydanticv2
def test_openapi_schema(client: TestClient): def test_openapi_schema(client: TestClient):
response = client.get("openapi.json") response = client.get("openapi.json")
assert response.json() == snapshot( assert response.json() == snapshot(

View File

@ -14,10 +14,9 @@ from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from .utils import needs_py310, needs_py_lt_314, needs_pydanticv2 from .utils import needs_py310, needs_py_lt_314
@needs_pydanticv2
def test_model_field_default_required(): def test_model_field_default_required():
from fastapi._compat import v2 from fastapi._compat import v2
@ -46,7 +45,6 @@ def test_is_model_field():
assert not _is_model_field(str) assert not _is_model_field(str)
@needs_pydanticv2
def test_get_model_config(): def test_get_model_config():
# For coverage in Pydantic v2 # For coverage in Pydantic v2
class Foo(BaseModel): class Foo(BaseModel):
@ -75,7 +73,6 @@ def test_complex():
assert response2.json() == [1, 2] assert response2.json() == [1, 2]
@needs_pydanticv2
def test_propagates_pydantic2_model_config(): def test_propagates_pydantic2_model_config():
app = FastAPI() app = FastAPI()
@ -136,7 +133,6 @@ def test_is_uploadfile_sequence_annotation():
assert is_uploadfile_sequence_annotation(Union[list[str], list[UploadFile]]) assert is_uploadfile_sequence_annotation(Union[list[str], list[UploadFile]])
@needs_pydanticv2
def test_serialize_sequence_value_with_optional_list(): def test_serialize_sequence_value_with_optional_list():
"""Test that serialize_sequence_value handles optional lists correctly.""" """Test that serialize_sequence_value handles optional lists correctly."""
from fastapi._compat import v2 from fastapi._compat import v2
@ -148,7 +144,6 @@ def test_serialize_sequence_value_with_optional_list():
assert isinstance(result, list) assert isinstance(result, list)
@needs_pydanticv2
@needs_py310 @needs_py310
def test_serialize_sequence_value_with_optional_list_pipe_union(): def test_serialize_sequence_value_with_optional_list_pipe_union():
"""Test that serialize_sequence_value handles optional lists correctly (with new syntax).""" """Test that serialize_sequence_value handles optional lists correctly (with new syntax)."""
@ -161,7 +156,6 @@ def test_serialize_sequence_value_with_optional_list_pipe_union():
assert isinstance(result, list) assert isinstance(result, list)
@needs_pydanticv2
def test_serialize_sequence_value_with_none_first_in_union(): def test_serialize_sequence_value_with_none_first_in_union():
"""Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" """Test that serialize_sequence_value handles Union[None, List[...]] correctly."""
from fastapi._compat import v2 from fastapi._compat import v2

View File

@ -1,9 +1,10 @@
import sys import sys
import warnings
from typing import Optional from typing import Optional
import pytest import pytest
from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14): if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
@ -33,6 +34,8 @@ class Item(BaseModel):
app = FastAPI() app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/items/{item_id}") @app.get("/items/{item_id}")
def get_item_with_path( def get_item_with_path(
@ -40,18 +43,17 @@ def get_item_with_path(
): ):
return {"item_id": item_id} return {"item_id": item_id}
@app.get("/items/") @app.get("/items/")
def get_items_with_query( def get_items_with_query(
q: Annotated[ q: Annotated[
Optional[str], Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$") Optional[str],
Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$"),
] = None, ] = None,
skip: Annotated[int, Query(ge=0)] = 0, skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10,
): ):
return {"q": q, "skip": skip, "limit": limit} return {"q": q, "skip": skip, "limit": limit}
@app.get("/users/") @app.get("/users/")
def get_user_with_header( def get_user_with_header(
x_custom: Annotated[Optional[str], Header()] = None, x_custom: Annotated[Optional[str], Header()] = None,
@ -59,7 +61,6 @@ def get_user_with_header(
): ):
return {"x_custom": x_custom, "x_token": x_token} return {"x_custom": x_custom, "x_token": x_token}
@app.get("/cookies/") @app.get("/cookies/")
def get_cookies( def get_cookies(
session_id: Annotated[Optional[str], Cookie()] = None, session_id: Annotated[Optional[str], Cookie()] = None,
@ -67,24 +68,23 @@ def get_cookies(
): ):
return {"session_id": session_id, "tracking_id": tracking_id} return {"session_id": session_id, "tracking_id": tracking_id}
@app.post("/items/") @app.post("/items/")
def create_item( def create_item(
item: Annotated[ item: Annotated[
Item, Item,
Body(examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]), Body(
examples=[{"name": "Foo", "price": 35.4, "description": "The Foo item"}]
),
], ],
): ):
return {"item": item} return {"item": item}
@app.post("/items-embed/") @app.post("/items-embed/")
def create_item_embed( def create_item_embed(
item: Annotated[Item, Body(embed=True)], item: Annotated[Item, Body(embed=True)],
): ):
return {"item": item} return {"item": item}
@app.put("/items/{item_id}") @app.put("/items/{item_id}")
def update_item( def update_item(
item_id: Annotated[int, Path(ge=1)], item_id: Annotated[int, Path(ge=1)],
@ -93,7 +93,6 @@ def update_item(
): ):
return {"item": item, "importance": importance} return {"item": item, "importance": importance}
@app.post("/form-data/") @app.post("/form-data/")
def submit_form( def submit_form(
username: Annotated[str, Form(min_length=3, max_length=50)], username: Annotated[str, Form(min_length=3, max_length=50)],
@ -102,7 +101,6 @@ def submit_form(
): ):
return {"username": username, "password": password, "email": email} return {"username": username, "password": password, "email": email}
@app.post("/upload/") @app.post("/upload/")
def upload_file( def upload_file(
file: Annotated[bytes, File()], file: Annotated[bytes, File()],
@ -110,7 +108,6 @@ def upload_file(
): ):
return {"file_size": len(file), "description": description} return {"file_size": len(file), "description": description}
@app.post("/upload-multiple/") @app.post("/upload-multiple/")
def upload_multiple_files( def upload_multiple_files(
files: Annotated[list[bytes], File()], files: Annotated[list[bytes], File()],
@ -211,10 +208,10 @@ def test_header_params_none():
# Cookie parameter tests # Cookie parameter tests
def test_cookie_params(): def test_cookie_params():
with TestClient(app) as client: with TestClient(app) as test_client:
client.cookies.set("session_id", "abc123") test_client.cookies.set("session_id", "abc123")
client.cookies.set("tracking_id", "1234567890abcdef") test_client.cookies.set("tracking_id", "1234567890abcdef")
response = client.get("/cookies/") response = test_client.get("/cookies/")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
"session_id": "abc123", "session_id": "abc123",
@ -223,9 +220,9 @@ def test_cookie_params():
def test_cookie_tracking_id_too_short(): def test_cookie_tracking_id_too_short():
with TestClient(app) as client: with TestClient(app) as test_client:
client.cookies.set("tracking_id", "short") test_client.cookies.set("tracking_id", "short")
response = client.get("/cookies/") response = test_client.get("/cookies/")
assert response.status_code == 422 assert response.status_code == 422
assert response.json() == snapshot( assert response.json() == snapshot(
{ {
@ -588,14 +585,7 @@ def test_openapi_schema():
"required": True, "required": True,
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v1=snapshot(
{
"$ref": "#/components/schemas/Body_update_item_items__item_id__put"
}
),
v2=snapshot(
{
"title": "Body", "title": "Body",
"allOf": [ "allOf": [
{ {
@ -603,8 +593,6 @@ def test_openapi_schema():
} }
], ],
} }
),
),
} }
}, },
}, },
@ -809,14 +797,7 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v1=snapshot(
{
"$ref": "#/components/schemas/Body_create_item_embed_items_embed__post"
}
),
v2=snapshot(
{
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post"
@ -824,8 +805,6 @@ def test_openapi_schema():
], ],
"title": "Body", "title": "Body",
} }
),
),
} }
}, },
"required": True, "required": True,
@ -855,14 +834,7 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/x-www-form-urlencoded": { "application/x-www-form-urlencoded": {
"schema": pydantic_snapshot( "schema": {
v1=snapshot(
{
"$ref": "#/components/schemas/Body_submit_form_form_data__post"
}
),
v2=snapshot(
{
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/Body_submit_form_form_data__post" "$ref": "#/components/schemas/Body_submit_form_form_data__post"
@ -870,8 +842,6 @@ def test_openapi_schema():
], ],
"title": "Body", "title": "Body",
} }
),
),
} }
}, },
"required": True, "required": True,
@ -901,23 +871,14 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"multipart/form-data": { "multipart/form-data": {
"schema": pydantic_snapshot( "schema": {
v1=snapshot(
{
"$ref": "#/components/schemas/Body_upload_file_upload__post"
}
),
v2=snapshot(
{
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/Body_upload_file_upload__post" "$ref": "#/components/schemas/Body_upload_file_upload__post"
} }
], ],
"title": "Body", "title": "Body",
} },
),
),
} }
}, },
"required": True, "required": True,
@ -947,14 +908,7 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"multipart/form-data": { "multipart/form-data": {
"schema": pydantic_snapshot( "schema": {
v1=snapshot(
{
"$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post"
}
),
v2=snapshot(
{
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post"
@ -962,8 +916,6 @@ def test_openapi_schema():
], ],
"title": "Body", "title": "Body",
} }
),
),
} }
}, },
"required": True, "required": True,
@ -990,21 +942,12 @@ def test_openapi_schema():
"components": { "components": {
"schemas": { "schemas": {
"Body_create_item_embed_items_embed__post": { "Body_create_item_embed_items_embed__post": {
"properties": pydantic_snapshot( "properties": {
v1=snapshot(
{"item": {"$ref": "#/components/schemas/Item"}}
),
v2=snapshot(
{
"item": { "item": {
"allOf": [ "allOf": [{"$ref": "#/components/schemas/Item"}],
{"$ref": "#/components/schemas/Item"}
],
"title": "Item", "title": "Item",
} }
} },
),
),
"type": "object", "type": "object",
"required": ["item"], "required": ["item"],
"title": "Body_create_item_embed_items_embed__post", "title": "Body_create_item_embed_items_embed__post",
@ -1030,17 +973,10 @@ def test_openapi_schema():
}, },
"Body_update_item_items__item_id__put": { "Body_update_item_items__item_id__put": {
"properties": { "properties": {
"item": pydantic_snapshot( "item": {
v1=snapshot({"$ref": "#/components/schemas/Item"}), "allOf": [{"$ref": "#/components/schemas/Item"}],
v2=snapshot(
{
"allOf": [
{"$ref": "#/components/schemas/Item"}
],
"title": "Item", "title": "Item",
} },
),
),
"importance": { "importance": {
"type": "integer", "type": "integer",
"maximum": 10.0, "maximum": 10.0,

View File

@ -2,8 +2,6 @@ import pytest
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from .utils import needs_pydanticv2
@pytest.fixture(name="client") @pytest.fixture(name="client")
def get_client(request): def get_client(request):
@ -35,7 +33,6 @@ def get_client(request):
@pytest.mark.parametrize("client", [True, False], indirect=True) @pytest.mark.parametrize("client", [True, False], indirect=True)
@pytest.mark.parametrize("path", ["/", "/responses"]) @pytest.mark.parametrize("path", ["/", "/responses"])
@needs_pydanticv2
def test_get(client: TestClient, path: str): def test_get(client: TestClient, path: str):
response = client.get(path) response = client.get(path)
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
@ -43,7 +40,6 @@ def test_get(client: TestClient, path: str):
@pytest.mark.parametrize("client", [True, False], indirect=True) @pytest.mark.parametrize("client", [True, False], indirect=True)
@needs_pydanticv2
def test_openapi_schema(client: TestClient): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text

View File

@ -1,12 +1,8 @@
from typing import Annotated, Optional from typing import Annotated, Optional
from fastapi import FastAPI from fastapi import FastAPI
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel, WithJsonSchema
if PYDANTIC_V2:
from pydantic import WithJsonSchema
app = FastAPI() app = FastAPI()
@ -14,7 +10,6 @@ app = FastAPI()
class Item(BaseModel): class Item(BaseModel):
name: str name: str
if PYDANTIC_V2:
description: Annotated[ description: Annotated[
Optional[str], WithJsonSchema({"type": ["string", "null"]}) Optional[str], WithJsonSchema({"type": ["string", "null"]})
] = None ] = None
@ -24,13 +19,6 @@ class Item(BaseModel):
"x-something-internal": {"level": 4}, "x-something-internal": {"level": 4},
} }
} }
else:
description: Optional[str] = None # type: ignore[no-redef]
class Config:
schema_extra = {
"x-something-internal": {"level": 4},
}
@app.get("/foo", response_model=Item) @app.get("/foo", response_model=Item)
@ -55,7 +43,7 @@ item_schema = {
}, },
"description": { "description": {
"title": "Description", "title": "Description",
"type": ["string", "null"] if PYDANTIC_V2 else "string", "type": ["string", "null"],
}, },
}, },
} }

View File

@ -7,12 +7,6 @@ from fastapi.datastructures import Default
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
# TODO: remove when deprecating Pydantic v1
def test_upload_file_invalid():
with pytest.raises(ValueError):
UploadFile.validate("not a Starlette UploadFile")
def test_upload_file_invalid_pydantic_v2(): def test_upload_file_invalid_pydantic_v2():
with pytest.raises(ValueError): with pytest.raises(ValueError):
UploadFile._validate("not a Starlette UploadFile", {}) UploadFile._validate("not a Starlette UploadFile", {})

View File

@ -1,13 +1,13 @@
import warnings
from datetime import datetime, timezone from datetime import datetime, timezone
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel
from .utils import needs_pydanticv1, needs_pydanticv2 from .utils import needs_pydanticv1
@needs_pydanticv2
def test_pydanticv2(): def test_pydanticv2():
from pydantic import field_serializer from pydantic import field_serializer
@ -34,7 +34,9 @@ def test_pydanticv2():
# TODO: remove when deprecating Pydantic v1 # TODO: remove when deprecating Pydantic v1
@needs_pydanticv1 @needs_pydanticv1
def test_pydanticv1(): def test_pydanticv1():
class ModelWithDatetimeField(BaseModel): from pydantic import v1
class ModelWithDatetimeField(v1.BaseModel):
dt_field: datetime dt_field: datetime
class Config: class Config:
@ -47,6 +49,9 @@ def test_pydanticv1():
app = FastAPI() app = FastAPI()
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/model", response_model=ModelWithDatetimeField) @app.get("/model", response_model=ModelWithDatetimeField)
def get_model(): def get_model():
return model return model

View File

@ -1,7 +1,8 @@
import warnings
from typing import Optional from typing import Optional
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
from pydantic import BaseModel, validator from pydantic.v1 import BaseModel, validator
app = FastAPI() app = FastAPI()
@ -18,6 +19,7 @@ class ModelA(BaseModel):
name: str name: str
description: Optional[str] = None description: Optional[str] = None
model_b: ModelB model_b: ModelB
tags: dict[str, str] = {}
@validator("name") @validator("name")
def lower_username(cls, name: str, values): def lower_username(cls, name: str, values):
@ -30,6 +32,14 @@ async def get_model_c() -> ModelC:
return ModelC(username="test-user", password="test-password") return ModelC(username="test-user", password="test-password")
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/model/{name}", response_model=ModelA) @app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)): async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "model_b": model_c} return {
"name": name,
"description": "model-a-desc",
"model_b": model_c,
"tags": {"key1": "value1", "key2": "value2"},
}

View File

@ -1,6 +1,7 @@
import pytest import pytest
from fastapi.exceptions import ResponseValidationError from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from ..utils import needs_pydanticv1 from ..utils import needs_pydanticv1
@ -21,6 +22,7 @@ def test_filter_sub_model(client: TestClient):
"name": "modelA", "name": "modelA",
"description": "model-a-desc", "description": "model-a-desc",
"model_b": {"username": "test-user"}, "model_b": {"username": "test-user"},
"tags": {"key1": "value1", "key2": "value2"},
} }
@ -41,7 +43,8 @@ def test_validator_is_cloned(client: TestClient):
def test_openapi_schema(client: TestClient): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == snapshot(
{
"openapi": "3.1.0", "openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"paths": { "paths": {
@ -62,7 +65,9 @@ def test_openapi_schema(client: TestClient):
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/ModelA"} "schema": {
"$ref": "#/components/schemas/ModelA"
}
} }
}, },
}, },
@ -89,7 +94,9 @@ def test_openapi_schema(client: TestClient):
"detail": { "detail": {
"title": "Detail", "title": "Detail",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"}, "items": {
"$ref": "#/components/schemas/ValidationError"
},
} }
}, },
}, },
@ -101,13 +108,21 @@ def test_openapi_schema(client: TestClient):
"name": {"title": "Name", "type": "string"}, "name": {"title": "Name", "type": "string"},
"description": {"title": "Description", "type": "string"}, "description": {"title": "Description", "type": "string"},
"model_b": {"$ref": "#/components/schemas/ModelB"}, "model_b": {"$ref": "#/components/schemas/ModelB"},
"tags": {
"additionalProperties": {"type": "string"},
"type": "object",
"title": "Tags",
"default": {},
},
}, },
}, },
"ModelB": { "ModelB": {
"title": "ModelB", "title": "ModelB",
"required": ["username"], "required": ["username"],
"type": "object", "type": "object",
"properties": {"username": {"title": "Username", "type": "string"}}, "properties": {
"username": {"title": "Username", "type": "string"}
},
}, },
"ValidationError": { "ValidationError": {
"title": "ValidationError", "title": "ValidationError",
@ -128,3 +143,4 @@ def test_openapi_schema(client: TestClient):
} }
}, },
} }
)

View File

@ -5,8 +5,7 @@ from dirty_equals import HasRepr, IsDict, IsOneOf
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
from fastapi.exceptions import ResponseValidationError from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from .utils import needs_pydanticv2
@pytest.fixture(name="client") @pytest.fixture(name="client")
@ -25,6 +24,7 @@ def get_client():
name: str name: str
description: Optional[str] = None description: Optional[str] = None
foo: ModelB foo: ModelB
tags: dict[str, str] = {}
@field_validator("name") @field_validator("name")
def lower_username(cls, name: str, info: ValidationInfo): def lower_username(cls, name: str, info: ValidationInfo):
@ -37,13 +37,17 @@ def get_client():
@app.get("/model/{name}", response_model=ModelA) @app.get("/model/{name}", response_model=ModelA)
async def get_model_a(name: str, model_c=Depends(get_model_c)): async def get_model_a(name: str, model_c=Depends(get_model_c)):
return {"name": name, "description": "model-a-desc", "foo": model_c} return {
"name": name,
"description": "model-a-desc",
"foo": model_c,
"tags": {"key1": "value1", "key2": "value2"},
}
client = TestClient(app) client = TestClient(app)
return client return client
@needs_pydanticv2
def test_filter_sub_model(client: TestClient): def test_filter_sub_model(client: TestClient):
response = client.get("/model/modelA") response = client.get("/model/modelA")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
@ -51,10 +55,10 @@ def test_filter_sub_model(client: TestClient):
"name": "modelA", "name": "modelA",
"description": "model-a-desc", "description": "model-a-desc",
"foo": {"username": "test-user"}, "foo": {"username": "test-user"},
"tags": {"key1": "value1", "key2": "value2"},
} }
@needs_pydanticv2
def test_validator_is_cloned(client: TestClient): def test_validator_is_cloned(client: TestClient):
with pytest.raises(ResponseValidationError) as err: with pytest.raises(ResponseValidationError) as err:
client.get("/model/modelX") client.get("/model/modelX")
@ -79,11 +83,11 @@ def test_validator_is_cloned(client: TestClient):
] ]
@needs_pydanticv2
def test_openapi_schema(client: TestClient): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
assert response.json() == { assert response.json() == snapshot(
{
"openapi": "3.1.0", "openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"}, "info": {"title": "FastAPI", "version": "0.1.0"},
"paths": { "paths": {
@ -104,7 +108,9 @@ def test_openapi_schema(client: TestClient):
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/ModelA"} "schema": {
"$ref": "#/components/schemas/ModelA"
}
} }
}, },
}, },
@ -131,7 +137,9 @@ def test_openapi_schema(client: TestClient):
"detail": { "detail": {
"title": "Detail", "title": "Detail",
"type": "array", "type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"}, "items": {
"$ref": "#/components/schemas/ValidationError"
},
} }
}, },
}, },
@ -155,13 +163,21 @@ def test_openapi_schema(client: TestClient):
# TODO remove when deprecating Pydantic v1 # TODO remove when deprecating Pydantic v1
IsDict({"title": "Description", "type": "string"}), IsDict({"title": "Description", "type": "string"}),
"foo": {"$ref": "#/components/schemas/ModelB"}, "foo": {"$ref": "#/components/schemas/ModelB"},
"tags": {
"additionalProperties": {"type": "string"},
"type": "object",
"title": "Tags",
"default": {},
},
}, },
}, },
"ModelB": { "ModelB": {
"title": "ModelB", "title": "ModelB",
"required": ["username"], "required": ["username"],
"type": "object", "type": "object",
"properties": {"username": {"title": "Username", "type": "string"}}, "properties": {
"username": {"title": "Username", "type": "string"}
},
}, },
"ValidationError": { "ValidationError": {
"title": "ValidationError", "title": "ValidationError",
@ -182,3 +198,4 @@ def test_openapi_schema(client: TestClient):
} }
}, },
} }
)

View File

@ -2,7 +2,6 @@ from typing import Annotated, Optional
from dirty_equals import IsDict from dirty_equals import IsDict
from fastapi import FastAPI, Form from fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -20,12 +19,7 @@ class FormModel(BaseModel):
class FormModelExtraAllow(BaseModel): class FormModelExtraAllow(BaseModel):
param: str param: str
if PYDANTIC_V2:
model_config = {"extra": "allow"} model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
@app.post("/form/") @app.post("/form/")

View File

@ -1,17 +1,26 @@
from collections.abc import Iterator import warnings
from typing import Any
import fastapi._compat
import fastapi.openapi.utils
import pydantic.schema
import pytest import pytest
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from fastapi.testclient import TestClient
from starlette.testclient import TestClient from inline_snapshot import snapshot
from .utils import needs_pydanticv1 from .utils import needs_pydanticv1
@pytest.fixture(
name="client",
params=[
pytest.param("pydantic-v1", marks=needs_pydanticv1),
"pydantic-v2",
],
)
def client_fixture(request: pytest.FixtureRequest) -> TestClient:
if request.param == "pydantic-v1":
from pydantic.v1 import BaseModel
else:
from pydantic import BaseModel
class Address(BaseModel): class Address(BaseModel):
""" """
This is a public description of an Address This is a public description of an Address
@ -23,22 +32,60 @@ class Address(BaseModel):
city: str city: str
state_province: str state_province: str
class Facility(BaseModel): class Facility(BaseModel):
id: str id: str
address: Address address: Address
app = FastAPI() app = FastAPI()
client = TestClient(app) if request.param == "pydantic-v1":
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.get("/facilities/{facility_id}") @app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility: ... def get_facility(facility_id: str) -> Facility:
return Facility(
id=facility_id,
address=Address(
line_1="123 Main St", city="Anytown", state_province="CA"
),
)
else:
@app.get("/facilities/{facility_id}")
def get_facility(facility_id: str) -> Facility:
return Facility(
id=facility_id,
address=Address(
line_1="123 Main St", city="Anytown", state_province="CA"
),
)
client = TestClient(app)
return client
openapi_schema = { def test_get(client: TestClient):
response = client.get("/facilities/42")
assert response.status_code == 200, response.text
assert response.json() == {
"id": "42",
"address": {
"line_1": "123 Main St",
"city": "Anytown",
"state_province": "CA",
},
}
def test_openapi_schema(client: TestClient):
"""
Sanity check to ensure our app's openapi schema renders as we expect
"""
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"components": { "components": {
"schemas": { "schemas": {
"Address": { "Address": {
@ -47,7 +94,10 @@ openapi_schema = {
"properties": { "properties": {
"city": {"title": "City", "type": "string"}, "city": {"title": "City", "type": "string"},
"line_1": {"title": "Line 1", "type": "string"}, "line_1": {"title": "Line 1", "type": "string"},
"state_province": {"title": "State Province", "type": "string"}, "state_province": {
"title": "State Province",
"type": "string",
},
}, },
"required": ["line_1", "city", "state_province"], "required": ["line_1", "city", "state_province"],
"title": "Address", "title": "Address",
@ -65,7 +115,9 @@ openapi_schema = {
"HTTPValidationError": { "HTTPValidationError": {
"properties": { "properties": {
"detail": { "detail": {
"items": {"$ref": "#/components/schemas/ValidationError"}, "items": {
"$ref": "#/components/schemas/ValidationError"
},
"title": "Detail", "title": "Detail",
"type": "array", "type": "array",
} }
@ -76,7 +128,9 @@ openapi_schema = {
"ValidationError": { "ValidationError": {
"properties": { "properties": {
"loc": { "loc": {
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"title": "Location", "title": "Location",
"type": "array", "type": "array",
}, },
@ -107,7 +161,9 @@ openapi_schema = {
"200": { "200": {
"content": { "content": {
"application/json": { "application/json": {
"schema": {"$ref": "#/components/schemas/Facility"} "schema": {
"$ref": "#/components/schemas/Facility"
}
} }
}, },
"description": "Successful Response", "description": "Successful Response",
@ -128,54 +184,4 @@ openapi_schema = {
} }
}, },
} }
def test_openapi_schema():
"""
Sanity check to ensure our app's openapi schema renders as we expect
"""
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == openapi_schema
class SortedTypeSet(set):
"""
Set of Types whose `__iter__()` method yields results sorted by the type names
"""
def __init__(self, seq: set[type[Any]], *, sort_reversed: bool):
super().__init__(seq)
self.sort_reversed = sort_reversed
def __iter__(self) -> Iterator[type[Any]]:
members_sorted = sorted(
super().__iter__(),
key=lambda type_: type_.__name__,
reverse=self.sort_reversed,
) )
yield from members_sorted
@needs_pydanticv1
@pytest.mark.parametrize("sort_reversed", [True, False])
def test_model_description_escaped_with_formfeed(sort_reversed: bool):
"""
Regression test for bug fixed by https://github.com/fastapi/fastapi/pull/6039.
Test `get_model_definitions` with models passed in different order.
"""
from fastapi._compat import v1
all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes)
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 = v1.get_model_definitions(
flat_models=SortedTypeSet(flat_models, sort_reversed=sort_reversed),
model_name_map=model_name_map,
)
assert models["Address"]["description"] == expected_address_description

View File

@ -5,7 +5,7 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel
from .utils import needs_pydanticv1, needs_pydanticv2 from .utils import needs_pydanticv1
class MyUuid: class MyUuid:
@ -26,7 +26,6 @@ class MyUuid:
raise TypeError("vars() argument must have __dict__ attribute") raise TypeError("vars() argument must have __dict__ attribute")
@needs_pydanticv2
def test_pydanticv2(): def test_pydanticv2():
from pydantic import field_serializer from pydantic import field_serializer
@ -73,6 +72,8 @@ def test_pydanticv2():
# TODO: remove when deprecating Pydantic v1 # TODO: remove when deprecating Pydantic v1
@needs_pydanticv1 @needs_pydanticv1
def test_pydanticv1(): def test_pydanticv1():
from pydantic import v1
app = FastAPI() app = FastAPI()
@app.get("/fast_uuid") @app.get("/fast_uuid")
@ -84,7 +85,7 @@ def test_pydanticv1():
vars(asyncpg_uuid) vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid} return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(BaseModel): class SomeCustomClass(v1.BaseModel):
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str} json_encoders = {uuid.UUID: str}

View File

@ -8,11 +8,11 @@ from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional from typing import Optional
import pytest import pytest
from fastapi._compat import PYDANTIC_V2, Undefined from fastapi._compat import Undefined
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, ValidationError from pydantic import BaseModel, Field, ValidationError
from .utils import needs_pydanticv1, needs_pydanticv2 from .utils import needs_pydanticv1
class Person: class Person:
@ -59,12 +59,7 @@ class RoleEnum(Enum):
class ModelWithConfig(BaseModel): class ModelWithConfig(BaseModel):
role: Optional[RoleEnum] = None role: Optional[RoleEnum] = None
if PYDANTIC_V2:
model_config = {"use_enum_values": True} model_config = {"use_enum_values": True}
else:
class Config:
use_enum_values = True
class ModelWithAlias(BaseModel): class ModelWithAlias(BaseModel):
@ -89,6 +84,18 @@ def test_encode_dict():
} }
def test_encode_dict_include_exclude_list():
pet = {"name": "Firulais", "owner": {"name": "Foo"}}
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
assert jsonable_encoder(pet, include=["name"]) == {"name": "Firulais"}
assert jsonable_encoder(pet, exclude=["owner"]) == {"name": "Firulais"}
assert jsonable_encoder(pet, include=[]) == {}
assert jsonable_encoder(pet, exclude=[]) == {
"name": "Firulais",
"owner": {"name": "Foo"},
}
def test_encode_class(): def test_encode_class():
person = Person(name="Foo") person = Person(name="Foo")
pet = Pet(owner=person, name="Firulais") pet = Pet(owner=person, name="Firulais")
@ -130,7 +137,6 @@ def test_encode_unsupported():
jsonable_encoder(unserializable) jsonable_encoder(unserializable)
@needs_pydanticv2
def test_encode_custom_json_encoders_model_pydanticv2(): def test_encode_custom_json_encoders_model_pydanticv2():
from pydantic import field_serializer from pydantic import field_serializer
@ -153,7 +159,9 @@ def test_encode_custom_json_encoders_model_pydanticv2():
# TODO: remove when deprecating Pydantic v1 # TODO: remove when deprecating Pydantic v1
@needs_pydanticv1 @needs_pydanticv1
def test_encode_custom_json_encoders_model_pydanticv1(): def test_encode_custom_json_encoders_model_pydanticv1():
class ModelWithCustomEncoder(BaseModel): from pydantic import v1
class ModelWithCustomEncoder(v1.BaseModel):
dt_field: datetime dt_field: datetime
class Config: class Config:
@ -208,10 +216,12 @@ def test_encode_model_with_default():
@needs_pydanticv1 @needs_pydanticv1
def test_custom_encoders(): def test_custom_encoders():
from pydantic import v1
class safe_datetime(datetime): class safe_datetime(datetime):
pass pass
class MyModel(BaseModel): class MyModel(v1.BaseModel):
dt_field: safe_datetime dt_field: safe_datetime
instance = MyModel(dt_field=safe_datetime.now()) instance = MyModel(dt_field=safe_datetime.now())
@ -244,12 +254,7 @@ def test_encode_model_with_pure_path():
class ModelWithPath(BaseModel): class ModelWithPath(BaseModel):
path: PurePath path: PurePath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True} model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
test_path = PurePath("/foo", "bar") test_path = PurePath("/foo", "bar")
obj = ModelWithPath(path=test_path) obj = ModelWithPath(path=test_path)
@ -260,12 +265,7 @@ def test_encode_model_with_pure_posix_path():
class ModelWithPath(BaseModel): class ModelWithPath(BaseModel):
path: PurePosixPath path: PurePosixPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True} model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) obj = ModelWithPath(path=PurePosixPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "/foo/bar"} assert jsonable_encoder(obj) == {"path": "/foo/bar"}
@ -275,45 +275,44 @@ def test_encode_model_with_pure_windows_path():
class ModelWithPath(BaseModel): class ModelWithPath(BaseModel):
path: PureWindowsPath path: PureWindowsPath
if PYDANTIC_V2:
model_config = {"arbitrary_types_allowed": True} model_config = {"arbitrary_types_allowed": True}
else:
class Config:
arbitrary_types_allowed = True
obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) obj = ModelWithPath(path=PureWindowsPath("/foo", "bar"))
assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} assert jsonable_encoder(obj) == {"path": "\\foo\\bar"}
def test_encode_pure_path():
test_path = PurePath("/foo", "bar")
assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)}
@needs_pydanticv1 @needs_pydanticv1
def test_encode_root(): def test_encode_root():
class ModelWithRoot(BaseModel): from pydantic import v1
class ModelWithRoot(v1.BaseModel):
__root__: str __root__: str
model = ModelWithRoot(__root__="Foo") model = ModelWithRoot(__root__="Foo")
assert jsonable_encoder(model) == "Foo" assert jsonable_encoder(model) == "Foo"
@needs_pydanticv2
def test_decimal_encoder_float(): def test_decimal_encoder_float():
data = {"value": Decimal(1.23)} data = {"value": Decimal(1.23)}
assert jsonable_encoder(data) == {"value": 1.23} assert jsonable_encoder(data) == {"value": 1.23}
@needs_pydanticv2
def test_decimal_encoder_int(): def test_decimal_encoder_int():
data = {"value": Decimal(2)} data = {"value": Decimal(2)}
assert jsonable_encoder(data) == {"value": 2} assert jsonable_encoder(data) == {"value": 2}
@needs_pydanticv2
def test_decimal_encoder_nan(): def test_decimal_encoder_nan():
data = {"value": Decimal("NaN")} data = {"value": Decimal("NaN")}
assert isnan(jsonable_encoder(data)["value"]) assert isnan(jsonable_encoder(data)["value"])
@needs_pydanticv2
def test_decimal_encoder_infinity(): def test_decimal_encoder_infinity():
data = {"value": Decimal("Infinity")} data = {"value": Decimal("Infinity")}
assert isinf(jsonable_encoder(data)["value"]) assert isinf(jsonable_encoder(data)["value"])
@ -330,7 +329,6 @@ def test_encode_deque_encodes_child_models():
assert jsonable_encoder(dq)[0]["test"] == "test" assert jsonable_encoder(dq)[0]["test"] == "test"
@needs_pydanticv2
def test_encode_pydantic_undefined(): def test_encode_pydantic_undefined():
data = {"value": Undefined} data = {"value": Undefined}
assert jsonable_encoder(data) == {"value": None} assert jsonable_encoder(data) == {"value": None}

View File

@ -9,8 +9,6 @@ from fastapi.testclient import TestClient
from inline_snapshot import snapshot from inline_snapshot import snapshot
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from tests.utils import pydantic_snapshot
class MessageEventType(str, Enum): class MessageEventType(str, Enum):
alpha = "alpha" alpha = "alpha"
@ -126,47 +124,21 @@ def test_openapi_schema():
}, },
"MessageEvent": { "MessageEvent": {
"properties": { "properties": {
"event_type": pydantic_snapshot( "event_type": {
v2=snapshot(
{
"$ref": "#/components/schemas/MessageEventType", "$ref": "#/components/schemas/MessageEventType",
"default": "alpha", "default": "alpha",
} },
),
v1=snapshot(
{
"allOf": [
{
"$ref": "#/components/schemas/MessageEventType"
}
],
"default": "alpha",
}
),
),
"output": {"type": "string", "title": "Output"}, "output": {"type": "string", "title": "Output"},
}, },
"type": "object", "type": "object",
"required": ["output"], "required": ["output"],
"title": "MessageEvent", "title": "MessageEvent",
}, },
"MessageEventType": pydantic_snapshot( "MessageEventType": {
v2=snapshot(
{
"type": "string", "type": "string",
"enum": ["alpha", "beta"], "enum": ["alpha", "beta"],
"title": "MessageEventType", "title": "MessageEventType",
} },
),
v1=snapshot(
{
"type": "string",
"enum": ["alpha", "beta"],
"title": "MessageEventType",
"description": "An enumeration.",
}
),
),
"MessageOutput": { "MessageOutput": {
"properties": { "properties": {
"body": {"type": "string", "title": "Body", "default": ""}, "body": {"type": "string", "title": "Body", "default": ""},

View File

@ -3,16 +3,13 @@ from typing import Optional
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from inline_snapshot import snapshot from inline_snapshot import snapshot
from pydantic import BaseModel from pydantic import BaseModel, computed_field
from .utils import PYDANTIC_V2, needs_pydanticv2
class SubItem(BaseModel): class SubItem(BaseModel):
subname: str subname: str
sub_description: Optional[str] = None sub_description: Optional[str] = None
tags: list[str] = [] tags: list[str] = []
if PYDANTIC_V2:
model_config = {"json_schema_serialization_defaults_required": True} model_config = {"json_schema_serialization_defaults_required": True}
@ -20,13 +17,9 @@ class Item(BaseModel):
name: str name: str
description: Optional[str] = None description: Optional[str] = None
sub: Optional[SubItem] = None sub: Optional[SubItem] = None
if PYDANTIC_V2:
model_config = {"json_schema_serialization_defaults_required": True} model_config = {"json_schema_serialization_defaults_required": True}
if PYDANTIC_V2:
from pydantic import computed_field
class WithComputedField(BaseModel): class WithComputedField(BaseModel):
name: str name: str
@ -58,8 +51,6 @@ def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
Item(name="Plumbus"), Item(name="Plumbus"),
] ]
if PYDANTIC_V2:
@app.post("/with-computed-field/") @app.post("/with-computed-field/")
def create_with_computed_field( def create_with_computed_field(
with_computed_field: WithComputedField, with_computed_field: WithComputedField,
@ -151,7 +142,6 @@ def test_read_items():
) )
@needs_pydanticv2
def test_with_computed_field(): def test_with_computed_field():
client = get_app_client() client = get_app_client()
client_no = get_app_client(separate_input_output_schemas=False) client_no = get_app_client(separate_input_output_schemas=False)
@ -168,7 +158,6 @@ def test_with_computed_field():
) )
@needs_pydanticv2
def test_openapi_schema(): def test_openapi_schema():
client = get_app_client() client = get_app_client()
response = client.get("/openapi.json") response = client.get("/openapi.json")
@ -449,7 +438,6 @@ def test_openapi_schema():
) )
@needs_pydanticv2
def test_openapi_schema_no_separate(): def test_openapi_schema_no_separate():
client = get_app_client(separate_input_output_schemas=False) client = get_app_client(separate_input_output_schemas=False)
response = client.get("/openapi.json") response = client.get("/openapi.json")

View File

@ -0,0 +1,98 @@
import sys
import pytest
from tests.utils import 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
def test_warns_pydantic_v1_model_in_endpoint_param() -> None:
class ParamModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
DeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the param data:",
):
@app.post("/param")
def endpoint(data: ParamModelV1):
return data
client = TestClient(app)
response = client.post("/param", json={"name": "test"})
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_return_type() -> None:
class ReturnModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
DeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the response model",
):
@app.get("/return")
def endpoint() -> ReturnModelV1:
return ReturnModelV1(name="test")
client = TestClient(app)
response = client.get("/return")
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_response_model() -> None:
class ResponseModelV1(BaseModel):
name: str
app = FastAPI()
with pytest.warns(
DeprecationWarning,
match=r"pydantic\.v1 is deprecated.*Please update the response model",
):
@app.get("/response-model", response_model=ResponseModelV1)
def endpoint():
return {"name": "test"}
client = TestClient(app)
response = client.get("/response-model")
assert response.status_code == 200, response.text
assert response.json() == {"name": "test"}
def test_warns_pydantic_v1_model_in_additional_responses_model() -> None:
class ErrorModelV1(BaseModel):
detail: str
app = FastAPI()
with pytest.warns(
DeprecationWarning,
match=r"pydantic\.v1 is deprecated.*In responses=\{\}, please update",
):
@app.get(
"/responses", response_model=None, responses={400: {"model": ErrorModelV1}}
)
def endpoint():
return {"ok": True}
client = TestClient(app)
response = client.get("/responses")
assert response.status_code == 200, response.text
assert response.json() == {"ok": True}

View File

@ -1,7 +1,8 @@
import sys import sys
import warnings
from typing import Any, Union from typing import Any, Union
from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14): if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
@ -26,24 +27,23 @@ class Item(BaseModel):
app = FastAPI() app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/simple-model") @app.post("/simple-model")
def handle_simple_model(data: SubItem) -> SubItem: def handle_simple_model(data: SubItem) -> SubItem:
return data return data
@app.post("/simple-model-filter", response_model=SubItem) @app.post("/simple-model-filter", response_model=SubItem)
def handle_simple_model_filter(data: SubItem) -> Any: def handle_simple_model_filter(data: SubItem) -> Any:
extended_data = data.dict() extended_data = data.dict()
extended_data.update({"secret_price": 42}) extended_data.update({"secret_price": 42})
return extended_data return extended_data
@app.post("/item") @app.post("/item")
def handle_item(data: Item) -> Item: def handle_item(data: Item) -> Item:
return data return data
@app.post("/item-filter", response_model=Item) @app.post("/item-filter", response_model=Item)
def handle_item_filter(data: Item) -> Any: def handle_item_filter(data: Item) -> Any:
extended_data = data.dict() extended_data = data.dict()
@ -225,21 +225,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/SubItem"}
"$ref": "#/components/schemas/SubItem"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/SubItem"}
),
)
} }
}, },
"required": True, "required": True,
@ -275,21 +266,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/SubItem"}
"$ref": "#/components/schemas/SubItem"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/SubItem"}
),
)
} }
}, },
"required": True, "required": True,
@ -325,21 +307,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,
@ -373,21 +346,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,

View File

@ -1,7 +1,8 @@
import sys import sys
import warnings
from typing import Any, Union from typing import Any, Union
from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14): if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
@ -27,11 +28,13 @@ class Item(BaseModel):
app = FastAPI() app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/item") @app.post("/item")
def handle_item(data: Item) -> list[Item]: def handle_item(data: Item) -> list[Item]:
return [data, data] return [data, data]
@app.post("/item-filter", response_model=list[Item]) @app.post("/item-filter", response_model=list[Item])
def handle_item_filter(data: Item) -> Any: def handle_item_filter(data: Item) -> Any:
extended_data = data.dict() extended_data = data.dict()
@ -39,14 +42,12 @@ def handle_item_filter(data: Item) -> Any:
extended_data["sub"].update({"internal_id": 67890}) extended_data["sub"].update({"internal_id": 67890})
return [extended_data, extended_data] return [extended_data, extended_data]
@app.post("/item-list") @app.post("/item-list")
def handle_item_list(data: list[Item]) -> Item: def handle_item_list(data: list[Item]) -> Item:
if data: if data:
return data[0] return data[0]
return Item(title="", size=0, sub=SubItem(name="")) return Item(title="", size=0, sub=SubItem(name=""))
@app.post("/item-list-filter", response_model=Item) @app.post("/item-list-filter", response_model=Item)
def handle_item_list_filter(data: list[Item]) -> Any: def handle_item_list_filter(data: list[Item]) -> Any:
if data: if data:
@ -56,12 +57,10 @@ def handle_item_list_filter(data: list[Item]) -> Any:
return extended_data return extended_data
return Item(title="", size=0, sub=SubItem(name="")) return Item(title="", size=0, sub=SubItem(name=""))
@app.post("/item-list-to-list") @app.post("/item-list-to-list")
def handle_item_list_to_list(data: list[Item]) -> list[Item]: def handle_item_list_to_list(data: list[Item]) -> list[Item]:
return data return data
@app.post("/item-list-to-list-filter", response_model=list[Item]) @app.post("/item-list-to-list-filter", response_model=list[Item])
def handle_item_list_to_list_filter(data: list[Item]) -> Any: def handle_item_list_to_list_filter(data: list[Item]) -> Any:
if data: if data:
@ -375,21 +374,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,
@ -429,21 +419,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,

View File

@ -1,7 +1,8 @@
import sys import sys
import warnings
from typing import Any, Union from typing import Any, Union
from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14): if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
@ -39,6 +40,8 @@ class NewItem(NewBaseModel):
app = FastAPI() app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/v1-to-v2/item") @app.post("/v1-to-v2/item")
def handle_v1_item_to_v2(data: Item) -> NewItem: def handle_v1_item_to_v2(data: Item) -> NewItem:
@ -50,22 +53,24 @@ def handle_v1_item_to_v2(data: Item) -> NewItem:
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
) )
@app.post("/v1-to-v2/item-filter", response_model=NewItem) @app.post("/v1-to-v2/item-filter", response_model=NewItem)
def handle_v1_item_to_v2_filter(data: Item) -> Any: def handle_v1_item_to_v2_filter(data: Item) -> Any:
result = { result = {
"new_title": data.title, "new_title": data.title,
"new_size": data.size, "new_size": data.size,
"new_description": data.description, "new_description": data.description,
"new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, "new_sub": {
"new_sub_name": data.sub.name,
"new_sub_secret": "sub_hidden",
},
"new_multi": [ "new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
for s in data.multi
], ],
"secret": "hidden_v1_to_v2", "secret": "hidden_v1_to_v2",
} }
return result return result
@app.post("/v2-to-v1/item") @app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: NewItem) -> Item: def handle_v2_item_to_v1(data: NewItem) -> Item:
return Item( return Item(
@ -76,7 +81,6 @@ def handle_v2_item_to_v1(data: NewItem) -> Item:
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
) )
@app.post("/v2-to-v1/item-filter", response_model=Item) @app.post("/v2-to-v1/item-filter", response_model=Item)
def handle_v2_item_to_v1_filter(data: NewItem) -> Any: def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
result = { result = {
@ -85,13 +89,13 @@ def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
"description": data.new_description, "description": data.new_description,
"sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
"multi": [ "multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi {"name": s.new_sub_name, "sub_secret": "sub_hidden"}
for s in data.new_multi
], ],
"secret": "hidden_v2_to_v1", "secret": "hidden_v2_to_v1",
} }
return result return result
@app.post("/v1-to-v2/item-to-list") @app.post("/v1-to-v2/item-to-list")
def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]: def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]:
converted = NewItem( converted = NewItem(
@ -103,7 +107,6 @@ def handle_v1_item_to_v2_list(data: Item) -> list[NewItem]:
) )
return [converted, converted] return [converted, converted]
@app.post("/v1-to-v2/list-to-list") @app.post("/v1-to-v2/list-to-list")
def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]: def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]:
result = [] result = []
@ -119,7 +122,6 @@ def handle_v1_list_to_v2_list(data: list[Item]) -> list[NewItem]:
) )
return result return result
@app.post("/v1-to-v2/list-to-list-filter", response_model=list[NewItem]) @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: def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any:
result = [] result = []
@ -128,7 +130,10 @@ def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any:
"new_title": item.title, "new_title": item.title,
"new_size": item.size, "new_size": item.size,
"new_description": item.description, "new_description": item.description,
"new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"}, "new_sub": {
"new_sub_name": item.sub.name,
"new_sub_secret": "sub_hidden",
},
"new_multi": [ "new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
for s in item.multi for s in item.multi
@ -138,7 +143,6 @@ def handle_v1_list_to_v2_list_filter(data: list[Item]) -> Any:
result.append(converted) result.append(converted)
return result return result
@app.post("/v1-to-v2/list-to-item") @app.post("/v1-to-v2/list-to-item")
def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem: def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem:
if data: if data:
@ -152,7 +156,6 @@ def handle_v1_list_to_v2_item(data: list[Item]) -> NewItem:
) )
return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name="")) return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name=""))
@app.post("/v2-to-v1/item-to-list") @app.post("/v2-to-v1/item-to-list")
def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]: def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]:
converted = Item( converted = Item(
@ -164,7 +167,6 @@ def handle_v2_item_to_v1_list(data: NewItem) -> list[Item]:
) )
return [converted, converted] return [converted, converted]
@app.post("/v2-to-v1/list-to-list") @app.post("/v2-to-v1/list-to-list")
def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]: def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]:
result = [] result = []
@ -180,7 +182,6 @@ def handle_v2_list_to_v1_list(data: list[NewItem]) -> list[Item]:
) )
return result return result
@app.post("/v2-to-v1/list-to-list-filter", response_model=list[Item]) @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: def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any:
result = [] result = []
@ -189,7 +190,10 @@ def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any:
"title": item.new_title, "title": item.new_title,
"size": item.new_size, "size": item.new_size,
"description": item.new_description, "description": item.new_description,
"sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, "sub": {
"name": item.new_sub.new_sub_name,
"sub_secret": "sub_hidden",
},
"multi": [ "multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"} {"name": s.new_sub_name, "sub_secret": "sub_hidden"}
for s in item.new_multi for s in item.new_multi
@ -199,7 +203,6 @@ def handle_v2_list_to_v1_list_filter(data: list[NewItem]) -> Any:
result.append(converted) result.append(converted)
return result return result
@app.post("/v2-to-v1/list-to-item") @app.post("/v2-to-v1/list-to-item")
def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item: def handle_v2_list_to_v1_item(data: list[NewItem]) -> Item:
if data: if data:
@ -668,9 +671,7 @@ def test_v2_to_v1_validation_error():
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == snapshot( assert response.json() == snapshot(
{ {
"detail": pydantic_snapshot( "detail": [
v2=snapshot(
[
{ {
"type": "missing", "type": "missing",
"loc": ["body", "new_size"], "loc": ["body", "new_size"],
@ -684,22 +685,6 @@ def test_v2_to_v1_validation_error():
"input": {"new_title": "Missing fields"}, "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",
},
]
),
)
} }
) )
@ -717,23 +702,12 @@ def test_v2_to_v1_nested_validation_error():
assert response.json() == snapshot( assert response.json() == snapshot(
{ {
"detail": [ "detail": [
pydantic_snapshot(
v2=snapshot(
{ {
"type": "missing", "type": "missing",
"loc": ["body", "new_sub", "new_sub_name"], "loc": ["body", "new_sub", "new_sub_name"],
"msg": "Field required", "msg": "Field required",
"input": {"wrong_field": "value"}, "input": {"wrong_field": "value"},
} }
),
v1=snapshot(
{
"loc": ["body", "new_sub", "new_sub_name"],
"msg": "field required",
"type": "value_error.missing",
}
),
)
] ]
} }
) )
@ -777,9 +751,7 @@ def test_v2_list_validation_error():
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == snapshot( assert response.json() == snapshot(
{ {
"detail": pydantic_snapshot( "detail": [
v2=snapshot(
[
{ {
"type": "missing", "type": "missing",
"loc": ["body", 1, "new_size"], "loc": ["body", 1, "new_size"],
@ -793,22 +765,6 @@ def test_v2_list_validation_error():
"input": {"new_title": "Invalid"}, "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",
},
]
),
)
} }
) )
@ -844,9 +800,7 @@ def test_invalid_list_structure_v2():
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == snapshot( assert response.json() == snapshot(
{ {
"detail": pydantic_snapshot( "detail": [
v2=snapshot(
[
{ {
"type": "list_type", "type": "list_type",
"loc": ["body"], "loc": ["body"],
@ -858,17 +812,6 @@ def test_invalid_list_structure_v2():
}, },
} }
] ]
),
v1=snapshot(
[
{
"loc": ["body"],
"msg": "value is not a valid list",
"type": "type_error.list",
}
]
),
)
} }
) )
@ -888,21 +831,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,
@ -938,21 +872,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,
@ -1056,21 +981,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,
@ -1440,17 +1356,10 @@ def test_openapi_schema():
"properties": { "properties": {
"new_title": {"type": "string", "title": "New Title"}, "new_title": {"type": "string", "title": "New Title"},
"new_size": {"type": "integer", "title": "New Size"}, "new_size": {"type": "integer", "title": "New Size"},
"new_description": pydantic_snapshot( "new_description": {
v2=snapshot(
{
"anyOf": [{"type": "string"}, {"type": "null"}], "anyOf": [{"type": "string"}, {"type": "null"}],
"title": "New Description", "title": "New Description",
} },
),
v1=snapshot(
{"type": "string", "title": "New Description"}
),
),
"new_sub": {"$ref": "#/components/schemas/NewSubItem"}, "new_sub": {"$ref": "#/components/schemas/NewSubItem"},
"new_multi": { "new_multi": {
"items": {"$ref": "#/components/schemas/NewSubItem"}, "items": {"$ref": "#/components/schemas/NewSubItem"},

View File

@ -1,9 +1,13 @@
import warnings
from fastapi import FastAPI from fastapi import FastAPI
from . import modelsv1, modelsv2, modelsv2b from . import modelsv1, modelsv2, modelsv2b
app = FastAPI() app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/v1-to-v2/item") @app.post("/v1-to-v2/item")
def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item: def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item:
@ -15,7 +19,6 @@ def handle_v1_item_to_v2(data: modelsv1.Item) -> modelsv2.Item:
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi], new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in data.multi],
) )
@app.post("/v2-to-v1/item") @app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item: def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item:
return modelsv1.Item( return modelsv1.Item(
@ -26,7 +29,6 @@ def handle_v2_item_to_v1(data: modelsv2.Item) -> modelsv1.Item:
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi], multi=[modelsv1.SubItem(name=s.new_sub_name) for s in data.new_multi],
) )
@app.post("/v1-to-v2/item-to-list") @app.post("/v1-to-v2/item-to-list")
def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]: def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]:
converted = modelsv2.Item( converted = modelsv2.Item(
@ -38,7 +40,6 @@ def handle_v1_item_to_v2_list(data: modelsv1.Item) -> list[modelsv2.Item]:
) )
return [converted, converted] return [converted, converted]
@app.post("/v1-to-v2/list-to-list") @app.post("/v1-to-v2/list-to-list")
def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]: def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]:
result = [] result = []
@ -49,12 +50,13 @@ def handle_v1_list_to_v2_list(data: list[modelsv1.Item]) -> list[modelsv2.Item]:
new_size=item.size, new_size=item.size,
new_description=item.description, new_description=item.description,
new_sub=modelsv2.SubItem(new_sub_name=item.sub.name), new_sub=modelsv2.SubItem(new_sub_name=item.sub.name),
new_multi=[modelsv2.SubItem(new_sub_name=s.name) for s in item.multi], new_multi=[
modelsv2.SubItem(new_sub_name=s.name) for s in item.multi
],
) )
) )
return result return result
@app.post("/v1-to-v2/list-to-item") @app.post("/v1-to-v2/list-to-item")
def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item: def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item:
if data: if data:
@ -70,7 +72,6 @@ def handle_v1_list_to_v2_item(data: list[modelsv1.Item]) -> modelsv2.Item:
new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="") new_title="", new_size=0, new_sub=modelsv2.SubItem(new_sub_name="")
) )
@app.post("/v2-to-v1/item-to-list") @app.post("/v2-to-v1/item-to-list")
def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]: def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]:
converted = modelsv1.Item( converted = modelsv1.Item(
@ -82,7 +83,6 @@ def handle_v2_item_to_v1_list(data: modelsv2.Item) -> list[modelsv1.Item]:
) )
return [converted, converted] return [converted, converted]
@app.post("/v2-to-v1/list-to-list") @app.post("/v2-to-v1/list-to-list")
def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]: def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]:
result = [] result = []
@ -93,12 +93,13 @@ def handle_v2_list_to_v1_list(data: list[modelsv2.Item]) -> list[modelsv1.Item]:
size=item.new_size, size=item.new_size,
description=item.new_description, description=item.new_description,
sub=modelsv1.SubItem(name=item.new_sub.new_sub_name), sub=modelsv1.SubItem(name=item.new_sub.new_sub_name),
multi=[modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi], multi=[
modelsv1.SubItem(name=s.new_sub_name) for s in item.new_multi
],
) )
) )
return result return result
@app.post("/v2-to-v1/list-to-item") @app.post("/v2-to-v1/list-to-item")
def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item: def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item:
if data: if data:
@ -112,7 +113,6 @@ def handle_v2_list_to_v1_item(data: list[modelsv2.Item]) -> modelsv1.Item:
) )
return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name="")) return modelsv1.Item(title="", size=0, sub=modelsv1.SubItem(name=""))
@app.post("/v2-to-v1/same-name") @app.post("/v2-to-v1/same-name")
def handle_v2_same_name_to_v1( def handle_v2_same_name_to_v1(
item1: modelsv2.Item, item2: modelsv2b.Item item1: modelsv2.Item, item2: modelsv2b.Item
@ -125,16 +125,13 @@ def handle_v2_same_name_to_v1(
multi=[modelsv1.SubItem(name=s.dup_sub_name) for s in item2.dup_multi], 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") @app.post("/v2-to-v1/list-of-items-to-list-of-items")
def handle_v2_items_in_list_to_v1_item_in_list( def handle_v2_items_in_list_to_v1_item_in_list(
data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList] data1: list[modelsv2.ItemInList], data2: list[modelsv2b.ItemInList]
) -> list[modelsv1.ItemInList]: ) -> list[modelsv1.ItemInList]:
result = []
item1 = data1[0] item1 = data1[0]
item2 = data2[0] item2 = data2[0]
result = [ return [
modelsv1.ItemInList(name1=item1.name2), modelsv1.ItemInList(name1=item1.name2),
modelsv1.ItemInList(name1=item2.dup_name2), modelsv1.ItemInList(name1=item2.dup_name2),
] ]
return result

View File

@ -1,6 +1,6 @@
import sys import sys
from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14): if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
@ -292,9 +292,7 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
@ -302,13 +300,6 @@ def test_openapi_schema():
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
}
),
)
} }
}, },
"required": True, "required": True,
@ -344,18 +335,9 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" "$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, "required": True,
@ -391,9 +373,7 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item" "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
@ -401,13 +381,6 @@ def test_openapi_schema():
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv1__Item"
}
),
)
} }
}, },
"required": True, "required": True,
@ -535,18 +508,9 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" "$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, "required": True,
@ -587,18 +551,9 @@ def test_openapi_schema():
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"items": pydantic_snapshot( "items": {
v2=snapshot(
{
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" "$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", "type": "array",
"title": "Data", "title": "Data",
} }
@ -642,18 +597,9 @@ def test_openapi_schema():
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"items": pydantic_snapshot( "items": {
v2=snapshot(
{
"$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__Item-Input" "$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", "type": "array",
"title": "Data", "title": "Data",
} }
@ -767,220 +713,7 @@ def test_openapi_schema():
}, },
}, },
"components": { "components": {
"schemas": pydantic_snapshot( "schemas": {
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": { "Body_handle_v2_items_in_list_to_v1_item_in_list_v2_to_v1_list_of_items_to_list_of_items_post": {
"properties": { "properties": {
"data1": { "data1": {
@ -1072,17 +805,13 @@ def test_openapi_schema():
"title": "Item", "title": "Item",
}, },
"tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": { "tests__test_pydantic_v1_v2_multifile__modelsv1__ItemInList": {
"properties": { "properties": {"name1": {"type": "string", "title": "Name1"}},
"name1": {"type": "string", "title": "Name1"}
},
"type": "object", "type": "object",
"required": ["name1"], "required": ["name1"],
"title": "ItemInList", "title": "ItemInList",
}, },
"tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": { "tests__test_pydantic_v1_v2_multifile__modelsv1__SubItem": {
"properties": { "properties": {"name": {"type": "string", "title": "Name"}},
"name": {"type": "string", "title": "Name"}
},
"type": "object", "type": "object",
"required": ["name"], "required": ["name"],
"title": "SubItem", "title": "SubItem",
@ -1148,9 +877,7 @@ def test_openapi_schema():
"title": "Item", "title": "Item",
}, },
"tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": { "tests__test_pydantic_v1_v2_multifile__modelsv2__ItemInList": {
"properties": { "properties": {"name2": {"type": "string", "title": "Name2"}},
"name2": {"type": "string", "title": "Name2"}
},
"type": "object", "type": "object",
"required": ["name2"], "required": ["name2"],
"title": "ItemInList", "title": "ItemInList",
@ -1218,9 +945,7 @@ def test_openapi_schema():
"required": ["dup_sub_name"], "required": ["dup_sub_name"],
"title": "SubItem", "title": "SubItem",
}, },
} },
),
),
}, },
} }
) )

View File

@ -1,7 +1,8 @@
import sys import sys
import warnings
from typing import Any, Union from typing import Any, Union
from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314 from tests.utils import skip_module_if_py_gte_314
if sys.version_info >= (3, 14): if sys.version_info >= (3, 14):
skip_module_if_py_gte_314() skip_module_if_py_gte_314()
@ -39,6 +40,8 @@ class NewItem(NewBaseModel):
app = FastAPI() app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/v1-to-v2/") @app.post("/v1-to-v2/")
def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]: def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]:
@ -52,7 +55,6 @@ def handle_v1_item_to_v2(data: Item) -> Union[NewItem, None]:
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi], 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]) @app.post("/v1-to-v2/item-filter", response_model=Union[NewItem, None])
def handle_v1_item_to_v2_filter(data: Item) -> Any: def handle_v1_item_to_v2_filter(data: Item) -> Any:
if data.size < 0: if data.size < 0:
@ -61,15 +63,18 @@ def handle_v1_item_to_v2_filter(data: Item) -> Any:
"new_title": data.title, "new_title": data.title,
"new_size": data.size, "new_size": data.size,
"new_description": data.description, "new_description": data.description,
"new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"}, "new_sub": {
"new_sub_name": data.sub.name,
"new_sub_secret": "sub_hidden",
},
"new_multi": [ "new_multi": [
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi {"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
for s in data.multi
], ],
"secret": "hidden_v1_to_v2", "secret": "hidden_v1_to_v2",
} }
return result return result
@app.post("/v2-to-v1/item") @app.post("/v2-to-v1/item")
def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]: def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]:
if data.new_size < 0: if data.new_size < 0:
@ -82,7 +87,6 @@ def handle_v2_item_to_v1(data: NewItem) -> Union[Item, None]:
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi], 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]) @app.post("/v2-to-v1/item-filter", response_model=Union[Item, None])
def handle_v2_item_to_v1_filter(data: NewItem) -> Any: def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
if data.new_size < 0: if data.new_size < 0:
@ -93,7 +97,8 @@ def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
"description": data.new_description, "description": data.new_description,
"sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"}, "sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
"multi": [ "multi": [
{"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi {"name": s.new_sub_name, "sub_secret": "sub_hidden"}
for s in data.new_multi
], ],
"secret": "hidden_v2_to_v1", "secret": "hidden_v2_to_v1",
} }
@ -312,9 +317,7 @@ def test_v2_to_v1_validation_error():
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
assert response.json() == snapshot( assert response.json() == snapshot(
{ {
"detail": pydantic_snapshot( "detail": [
v2=snapshot(
[
{ {
"type": "missing", "type": "missing",
"loc": ["body", "new_size"], "loc": ["body", "new_size"],
@ -328,22 +331,6 @@ def test_v2_to_v1_validation_error():
"input": {"new_title": "Missing fields"}, "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",
},
]
),
)
} }
) )
@ -361,23 +348,12 @@ def test_v2_to_v1_nested_validation_error():
assert response.json() == snapshot( assert response.json() == snapshot(
{ {
"detail": [ "detail": [
pydantic_snapshot(
v2=snapshot(
{ {
"type": "missing", "type": "missing",
"loc": ["body", "new_sub", "new_sub_name"], "loc": ["body", "new_sub", "new_sub_name"],
"msg": "Field required", "msg": "Field required",
"input": {"wrong_field": "value"}, "input": {"wrong_field": "value"},
} }
),
v1=snapshot(
{
"loc": ["body", "new_sub", "new_sub_name"],
"msg": "field required",
"type": "value_error.missing",
}
),
)
] ]
} }
) )
@ -396,23 +372,12 @@ def test_v2_to_v1_type_validation_error():
assert response.json() == snapshot( assert response.json() == snapshot(
{ {
"detail": [ "detail": [
pydantic_snapshot(
v2=snapshot(
{ {
"type": "int_parsing", "type": "int_parsing",
"loc": ["body", "new_size"], "loc": ["body", "new_size"],
"msg": "Input should be a valid integer, unable to parse string as an integer", "msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "not_a_number", "input": "not_a_number",
} }
),
v1=snapshot(
{
"loc": ["body", "new_size"],
"msg": "value is not a valid integer",
"type": "type_error.integer",
}
),
)
] ]
} }
) )
@ -483,21 +448,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,
@ -507,9 +463,7 @@ def test_openapi_schema():
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"anyOf": [ "anyOf": [
{ {
"$ref": "#/components/schemas/NewItem" "$ref": "#/components/schemas/NewItem"
@ -518,11 +472,6 @@ def test_openapi_schema():
], ],
"title": "Response Handle V1 Item To V2 V1 To V2 Post", "title": "Response Handle V1 Item To V2 V1 To V2 Post",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/NewItem"}
),
)
} }
}, },
}, },
@ -546,21 +495,12 @@ def test_openapi_schema():
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"allOf": [ "allOf": [
{ {"$ref": "#/components/schemas/Item"}
"$ref": "#/components/schemas/Item"
}
], ],
"title": "Data", "title": "Data",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/Item"}
),
)
} }
}, },
"required": True, "required": True,
@ -570,9 +510,7 @@ def test_openapi_schema():
"description": "Successful Response", "description": "Successful Response",
"content": { "content": {
"application/json": { "application/json": {
"schema": pydantic_snapshot( "schema": {
v2=snapshot(
{
"anyOf": [ "anyOf": [
{ {
"$ref": "#/components/schemas/NewItem" "$ref": "#/components/schemas/NewItem"
@ -581,11 +519,6 @@ def test_openapi_schema():
], ],
"title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post", "title": "Response Handle V1 Item To V2 Filter V1 To V2 Item Filter Post",
} }
),
v1=snapshot(
{"$ref": "#/components/schemas/NewItem"}
),
)
} }
}, },
}, },
@ -707,17 +640,10 @@ def test_openapi_schema():
"properties": { "properties": {
"new_title": {"type": "string", "title": "New Title"}, "new_title": {"type": "string", "title": "New Title"},
"new_size": {"type": "integer", "title": "New Size"}, "new_size": {"type": "integer", "title": "New Size"},
"new_description": pydantic_snapshot( "new_description": {
v2=snapshot(
{
"anyOf": [{"type": "string"}, {"type": "null"}], "anyOf": [{"type": "string"}, {"type": "null"}],
"title": "New Description", "title": "New Description",
} },
),
v1=snapshot(
{"type": "string", "title": "New Description"}
),
),
"new_sub": {"$ref": "#/components/schemas/NewSubItem"}, "new_sub": {"$ref": "#/components/schemas/NewSubItem"},
"new_multi": { "new_multi": {
"items": {"$ref": "#/components/schemas/NewSubItem"}, "items": {"$ref": "#/components/schemas/NewSubItem"},

View File

@ -1,5 +1,4 @@
from fastapi import Cookie, FastAPI, Header, Query from fastapi import Cookie, FastAPI, Header, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel
@ -9,12 +8,7 @@ app = FastAPI()
class Model(BaseModel): class Model(BaseModel):
param: str param: str
if PYDANTIC_V2:
model_config = {"extra": "allow"} model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
@app.get("/query") @app.get("/query")

View File

@ -1,13 +1,13 @@
import warnings
from typing import Any from typing import Any
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from .utils import needs_pydanticv1, needs_pydanticv2 from .utils import needs_pydanticv1
@needs_pydanticv2
def test_read_with_orm_mode() -> None: def test_read_with_orm_mode() -> None:
class PersonBase(BaseModel): class PersonBase(BaseModel):
name: str name: str
@ -48,7 +48,9 @@ def test_read_with_orm_mode() -> None:
@needs_pydanticv1 @needs_pydanticv1
def test_read_with_orm_mode_pv1() -> None: def test_read_with_orm_mode_pv1() -> None:
class PersonBase(BaseModel): from pydantic import v1
class PersonBase(v1.BaseModel):
name: str name: str
lastname: str lastname: str
@ -72,6 +74,9 @@ def test_read_with_orm_mode_pv1() -> None:
app = FastAPI() app = FastAPI()
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@app.post("/people/", response_model=PersonRead) @app.post("/people/", response_model=PersonRead)
def create_person(person: PersonCreate) -> Any: def create_person(person: PersonCreate) -> Any:
db_person = Person.from_orm(person) db_person = Person.from_orm(person)

View File

@ -1,6 +1,5 @@
from dirty_equals import IsPartialDict from dirty_equals import IsPartialDict
from fastapi import Cookie, FastAPI, Header, Query from fastapi import Cookie, FastAPI, Header, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -53,7 +52,6 @@ def test_query_model_with_alias_by_name():
response = client.get("/query", params={"param": "value"}) response = client.get("/query", params={"param": "value"})
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
details = response.json() details = response.json()
if PYDANTIC_V2:
assert details["detail"][0]["input"] == {"param": "value"} assert details["detail"][0]["input"] == {"param": "value"}
@ -62,7 +60,6 @@ def test_header_model_with_alias_by_name():
response = client.get("/header", headers={"param": "value"}) response = client.get("/header", headers={"param": "value"})
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
details = response.json() details = response.json()
if PYDANTIC_V2:
assert details["detail"][0]["input"] == IsPartialDict({"param": "value"}) assert details["detail"][0]["input"] == IsPartialDict({"param": "value"})
@ -72,5 +69,4 @@ def test_cookie_model_with_alias_by_name():
response = client.get("/cookie") response = client.get("/cookie")
assert response.status_code == 422, response.text assert response.status_code == 422, response.text
details = response.json() details = response.json()
if PYDANTIC_V2:
assert details["detail"][0]["input"] == {"param": "value"} assert details["detail"][0]["input"] == {"param": "value"}

View File

@ -6,8 +6,6 @@ from fastapi import Body, FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name from .utils import get_body_model_name
app = FastAPI() app = FastAPI()
@ -246,7 +244,6 @@ async def read_model_required_list_validation_alias(
return {"p": p.p} return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
["/required-list-validation-alias", "/model-required-list-validation-alias"], ["/required-list-validation-alias", "/model-required-list-validation-alias"],
@ -269,7 +266,6 @@ def test_required_list_validation_alias_schema(path: str):
} }
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}]) @pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
@ -294,7 +290,6 @@ def test_required_list_validation_alias_missing(path: str, json: Union[dict, Non
} }
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -319,7 +314,6 @@ def test_required_list_validation_alias_by_name(path: str):
} }
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -364,7 +358,6 @@ def read_model_required_list_alias_and_validation_alias(
return {"p": p.p} return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -390,7 +383,6 @@ def test_required_list_alias_and_validation_alias_schema(path: str):
} }
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}]) @pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
@ -415,7 +407,6 @@ def test_required_list_alias_and_validation_alias_missing(path: str, json):
} }
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -442,7 +433,6 @@ def test_required_list_alias_and_validation_alias_by_name(path: str):
} }
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -467,7 +457,6 @@ def test_required_list_alias_and_validation_alias_by_alias(path: str):
} }
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [

View File

@ -6,8 +6,6 @@ from fastapi import Body, FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name from .utils import get_body_model_name
app = FastAPI() app = FastAPI()
@ -283,7 +281,6 @@ def read_model_optional_list_validation_alias(
return {"p": p.p} return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"], ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
@ -369,7 +366,6 @@ def test_optional_list_validation_alias_missing_empty_dict(path: str):
assert response.json() == {"p": None} assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -384,7 +380,6 @@ def test_optional_list_validation_alias_by_name(path: str):
assert response.json() == {"p": None} assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -432,7 +427,6 @@ def read_model_optional_list_alias_and_validation_alias(
return {"p": p.p} return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -524,7 +518,6 @@ def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str):
assert response.json() == {"p": None} assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -539,7 +532,6 @@ def test_optional_list_alias_and_validation_alias_by_name(path: str):
assert response.json() == {"p": None} assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
@ -554,7 +546,6 @@ def test_optional_list_alias_and_validation_alias_by_alias(path: str):
assert response.json() == {"p": None} assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [

Some files were not shown because too many files have changed in this diff Show More