diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8157e364b..85f9c4afd 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -44,22 +44,45 @@ jobs:
run: bash scripts/lint.sh
test:
- runs-on: ubuntu-latest
strategy:
matrix:
- python-version:
- - "3.14"
- - "3.13"
- - "3.12"
- - "3.11"
- - "3.10"
- - "3.9"
- - "3.8"
- pydantic-version: ["pydantic-v1", "pydantic-v2"]
- exclude:
- - python-version: "3.14"
+ os: [ windows-latest, macos-latest ]
+ python-version: [ "3.14" ]
+ pydantic-version: [ "pydantic-v2" ]
+ include:
+ - os: macos-latest
+ python-version: "3.8"
pydantic-version: "pydantic-v1"
+ - os: windows-latest
+ python-version: "3.8"
+ pydantic-version: "pydantic-v2"
+ coverage: coverage
+ - os: ubuntu-latest
+ python-version: "3.9"
+ pydantic-version: "pydantic-v1"
+ coverage: coverage
+ - os: macos-latest
+ python-version: "3.10"
+ pydantic-version: "pydantic-v2"
+ - os: windows-latest
+ python-version: "3.11"
+ pydantic-version: "pydantic-v1"
+ - os: ubuntu-latest
+ python-version: "3.12"
+ pydantic-version: "pydantic-v2"
+ - os: macos-latest
+ python-version: "3.13"
+ pydantic-version: "pydantic-v1"
+ - os: windows-latest
+ python-version: "3.13"
+ pydantic-version: "pydantic-v2"
+ coverage: coverage
+ - os: ubuntu-latest
+ python-version: "3.14"
+ pydantic-version: "pydantic-v2"
+ coverage: coverage
fail-fast: false
+ runs-on: ${{ matrix.os }}
steps:
- name: Dump GitHub context
env:
@@ -96,10 +119,12 @@ jobs:
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
+ # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
- name: Store coverage files
+ if: matrix.coverage == 'coverage'
uses: actions/upload-artifact@v5
with:
- name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
+ name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
path: coverage
include-hidden-files: true
diff --git a/docs/en/docs/_llm-test.md b/docs/en/docs/_llm-test.md
index e72450b91..9f216f9d7 100644
--- a/docs/en/docs/_llm-test.md
+++ b/docs/en/docs/_llm-test.md
@@ -15,7 +15,7 @@ Use as follows:
The tests:
-## Code snippets { #code-snippets}
+## Code snippets { #code-snippets }
//// tab | Test
@@ -53,7 +53,7 @@ See for example section `### Quotes` in `docs/de/llm-prompt.md`.
////
-## Quotes in code snippets { #quotes-in-code-snippets}
+## Quotes in code snippets { #quotes-in-code-snippets }
//// tab | Test
diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md
index df03b7675..a0a5de3b7 100644
--- a/docs/en/docs/index.md
+++ b/docs/en/docs/index.md
@@ -52,13 +52,13 @@ The key features are:
-### Keystone Sponsor
+### Keystone Sponsor { #keystone-sponsor }
{% for sponsor in sponsors.keystone -%}
{% endfor -%}
-### Gold and Silver Sponsors
+### Gold and Silver Sponsors { #gold-and-silver-sponsors }
{% for sponsor in sponsors.gold -%}
diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md
index bb1e95aa9..02fae0e62 100644
--- a/docs/en/docs/release-notes.md
+++ b/docs/en/docs/release-notes.md
@@ -7,15 +7,38 @@ hide:
## Latest Changes
+## 0.124.4
+
+### Fixes
+
+* 🐛 Fix parameter aliases. PR [#14371](https://github.com/fastapi/fastapi/pull/14371) by [@YuriiMotov](https://github.com/YuriiMotov).
+
+## 0.124.3
+
+### Fixes
+
+* 🐛 Fix support for tagged union with discriminator inside of `Annotated` with `Body()`. PR [#14512](https://github.com/fastapi/fastapi/pull/14512) by [@tiangolo](https://github.com/tiangolo).
+
+### Refactors
+
+* ✅ Add set of tests for request parameters and alias. PR [#14358](https://github.com/fastapi/fastapi/pull/14358) by [@YuriiMotov](https://github.com/YuriiMotov).
+
### Docs
+* 📝 Tweak links format. PR [#14505](https://github.com/fastapi/fastapi/pull/14505) by [@tiangolo](https://github.com/tiangolo).
* 📝 Update docs about re-raising validation errors, do not include string as is to not leak information. PR [#14487](https://github.com/fastapi/fastapi/pull/14487) by [@tiangolo](https://github.com/tiangolo).
* 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo).
### Translations
+* 🌐 Sync Russian docs. PR [#14509](https://github.com/fastapi/fastapi/pull/14509) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🌐 Sync German docs. PR [#14488](https://github.com/fastapi/fastapi/pull/14488) by [@nilslindemann](https://github.com/nilslindemann).
+### Internal
+
+* 👷 Tweak coverage to not pass Smokeshow max file size limit. PR [#14507](https://github.com/fastapi/fastapi/pull/14507) by [@tiangolo](https://github.com/tiangolo).
+* ✅ Expand test matrix to include Windows and MacOS. PR [#14171](https://github.com/fastapi/fastapi/pull/14171) by [@svlandeg](https://github.com/svlandeg).
+
## 0.124.2
### Fixes
diff --git a/docs/ru/docs/_llm-test.md b/docs/ru/docs/_llm-test.md
index 476cc1924..9a15f6bb2 100644
--- a/docs/ru/docs/_llm-test.md
+++ b/docs/ru/docs/_llm-test.md
@@ -15,7 +15,7 @@
Тесты:
-## Фрагменты кода { #code-snippets}
+## Фрагменты кода { #code-snippets }
//// tab | Тест
@@ -53,7 +53,7 @@ LLM, вероятно, переведёт это неправильно. Инт
////
-## Кавычки во фрагментах кода { #quotes-in-code-snippets}
+## Кавычки во фрагментах кода { #quotes-in-code-snippets }
//// tab | Тест
diff --git a/docs/ru/docs/advanced/additional-responses.md b/docs/ru/docs/advanced/additional-responses.md
index c63c0c08b..1fc3715e4 100644
--- a/docs/ru/docs/advanced/additional-responses.md
+++ b/docs/ru/docs/advanced/additional-responses.md
@@ -175,7 +175,7 @@
Например, вы можете добавить дополнительный тип содержимого `image/png`, объявив, что ваша операция пути может возвращать JSON‑объект (с типом содержимого `application/json`) или PNG‑изображение:
-{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *}
+{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *}
/// note | Примечание
@@ -237,7 +237,7 @@ new_dict = {**old_dict, "new key": "new value"}
Например:
-{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *}
+{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *}
## Дополнительная информация об ответах OpenAPI { #more-information-about-openapi-responses }
diff --git a/docs/ru/docs/advanced/advanced-dependencies.md b/docs/ru/docs/advanced/advanced-dependencies.md
index 339c0a363..cc6691b30 100644
--- a/docs/ru/docs/advanced/advanced-dependencies.md
+++ b/docs/ru/docs/advanced/advanced-dependencies.md
@@ -144,7 +144,7 @@ checker(q="somequery")
### Фоновые задачи и зависимости с `yield`, технические детали { #background-tasks-and-dependencies-with-yield-technical-details }
-До FastAPI 0.106.0 вызывать исключения после `yield` было невозможно: код после `yield` в зависимостях выполнялся уже после отправки ответа, поэтому [Обработчики исключений](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} к тому моменту уже отработали.
+До FastAPI 0.106.0 вызывать исключения после `yield` было невозможно: код после `yield` в зависимостях выполнялся уже после отправки ответа, поэтому [Обработчики исключений](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} к тому моменту уже отработали.
Так было сделано в основном для того, чтобы можно было использовать те же объекты, «отданные» зависимостями через `yield`, внутри фоновых задач, потому что код после `yield` выполнялся после завершения фоновых задач.
diff --git a/docs/ru/docs/advanced/behind-a-proxy.md b/docs/ru/docs/advanced/behind-a-proxy.md
index 281cb7f73..7119efe2d 100644
--- a/docs/ru/docs/advanced/behind-a-proxy.md
+++ b/docs/ru/docs/advanced/behind-a-proxy.md
@@ -64,7 +64,7 @@ https://mysuperapp.com/items/
///
-### Как работают пересылаемые заголовки прокси
+### Как работают пересылаемые заголовки прокси { #how-proxy-forwarded-headers-work }
Ниже показано, как прокси добавляет пересылаемые заголовки между клиентом и сервером приложения:
@@ -443,6 +443,14 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
///
+/// note | Технические детали
+
+Свойство `servers` в спецификации OpenAPI является необязательным.
+
+Если вы не укажете параметр `servers`, а `root_path` равен `/`, то свойство `servers` в сгенерированной схеме OpenAPI по умолчанию будет опущено. Это эквивалентно серверу со значением `url` равным `/`.
+
+///
+
### Отключить автоматическое добавление сервера из `root_path` { #disable-automatic-server-from-root-path }
Если вы не хотите, чтобы FastAPI добавлял автоматический сервер, используя `root_path`, укажите параметр `root_path_in_servers=False`:
diff --git a/docs/ru/docs/advanced/dataclasses.md b/docs/ru/docs/advanced/dataclasses.md
index 816f74404..c37ce3023 100644
--- a/docs/ru/docs/advanced/dataclasses.md
+++ b/docs/ru/docs/advanced/dataclasses.md
@@ -4,7 +4,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
Но FastAPI также поддерживает использование `dataclasses` тем же способом:
-{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *}
+{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть встроенная поддержка `dataclasses`.
@@ -32,7 +32,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
Вы также можете использовать `dataclasses` в параметре `response_model`:
-{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *}
+{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
Этот dataclass будет автоматически преобразован в Pydantic dataclass.
@@ -48,7 +48,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
В таком случае вы можете просто заменить стандартные `dataclasses` на `pydantic.dataclasses`, которая является полностью совместимой заменой (drop-in replacement):
-{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *}
+{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
1. Мы по-прежнему импортируем `field` из стандартных `dataclasses`.
diff --git a/docs/ru/docs/advanced/openapi-callbacks.md b/docs/ru/docs/advanced/openapi-callbacks.md
index faf58370b..de7e28301 100644
--- a/docs/ru/docs/advanced/openapi-callbacks.md
+++ b/docs/ru/docs/advanced/openapi-callbacks.md
@@ -31,7 +31,7 @@
Эта часть вполне обычна, большая часть кода вам уже знакома:
-{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *}
+{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *}
/// tip | Совет
@@ -90,7 +90,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
Сначала создайте новый `APIRouter`, который будет содержать один или несколько обратных вызовов.
-{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *}
+{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *}
### Создайте *операцию пути* для обратного вызова { #create-the-callback-path-operation }
@@ -101,7 +101,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
* Вероятно, в ней должно быть объявление тела запроса, например `body: InvoiceEvent`.
* А также может быть объявление модели ответа, например `response_model=InvoiceEventReceived`.
-{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *}
+{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *}
Есть 2 основных отличия от обычной *операции пути*:
@@ -169,7 +169,7 @@ https://www.external.org/events/invoices/2expen51ve
Теперь используйте параметр `callbacks` в *декораторе операции пути вашего API*, чтобы передать атрибут `.routes` (это, по сути, просто `list` маршрутов/*операций пути*) из этого маршрутизатора обратных вызовов:
-{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *}
+{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *}
/// tip | Совет
diff --git a/docs/ru/docs/advanced/path-operation-advanced-configuration.md b/docs/ru/docs/advanced/path-operation-advanced-configuration.md
index fcb3cd47f..78a16a558 100644
--- a/docs/ru/docs/advanced/path-operation-advanced-configuration.md
+++ b/docs/ru/docs/advanced/path-operation-advanced-configuration.md
@@ -50,7 +50,7 @@
Эта часть не попадёт в документацию, но другие инструменты (например, Sphinx) смогут использовать остальное.
-{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *}
## Дополнительные ответы { #additional-responses }
@@ -155,13 +155,13 @@
//// tab | Pydantic v2
-{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
////
//// tab | Pydantic v1
-{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *}
////
@@ -179,13 +179,13 @@
//// tab | Pydantic v2
-{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
////
//// tab | Pydantic v1
-{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *}
+{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *}
////
diff --git a/docs/ru/docs/advanced/response-directly.md b/docs/ru/docs/advanced/response-directly.md
index febd40ed4..3c10633e9 100644
--- a/docs/ru/docs/advanced/response-directly.md
+++ b/docs/ru/docs/advanced/response-directly.md
@@ -34,7 +34,7 @@
В таких случаях вы можете использовать `jsonable_encoder` для преобразования данных перед передачей их в ответ:
-{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *}
+{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *}
/// note | Технические детали
diff --git a/docs/ru/docs/advanced/settings.md b/docs/ru/docs/advanced/settings.md
index a335548c3..0ef46fb13 100644
--- a/docs/ru/docs/advanced/settings.md
+++ b/docs/ru/docs/advanced/settings.md
@@ -148,7 +148,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p
Продолжая предыдущий пример, ваш файл `config.py` может выглядеть так:
-{* ../../docs_src/settings/app02/config.py hl[10] *}
+{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *}
Обратите внимание, что теперь мы не создаем экземпляр по умолчанию `settings = Settings()`.
@@ -174,7 +174,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p
Далее будет очень просто предоставить другой объект настроек во время тестирования, создав переопределение зависимости для `get_settings`:
-{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *}
+{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *}
В переопределении зависимости мы задаем новое значение `admin_email` при создании нового объекта `Settings`, а затем возвращаем этот новый объект.
@@ -217,7 +217,7 @@ APP_NAME="ChimichangApp"
//// tab | Pydantic v2
-{* ../../docs_src/settings/app03_an/config.py hl[9] *}
+{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
/// tip | Совет
@@ -229,7 +229,7 @@ APP_NAME="ChimichangApp"
//// tab | Pydantic v1
-{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *}
+{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *}
/// tip | Совет
diff --git a/docs/ru/docs/deployment/cloud.md b/docs/ru/docs/deployment/cloud.md
index a400d1843..955db2a15 100644
--- a/docs/ru/docs/deployment/cloud.md
+++ b/docs/ru/docs/deployment/cloud.md
@@ -4,11 +4,19 @@
В большинстве случаев у основных облачных провайдеров есть руководства по развертыванию FastAPI на их платформе.
+## FastAPI Cloud { #fastapi-cloud }
+
+**FastAPI Cloud** создан тем же автором и командой, стоящими за **FastAPI**.
+
+Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
+
+Он переносит тот же **опыт разработчика** создания приложений с FastAPI на их **развертывание** в облаке. 🎉
+
+FastAPI Cloud — основной спонсор и источник финансирования для open source проектов *FastAPI and friends*. ✨
+
## Облачные провайдеры — спонсоры { #cloud-providers-sponsors }
-Некоторые облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ — это обеспечивает непрерывное и здоровое развитие FastAPI и его экосистемы.
-
-И это показывает их искреннюю приверженность FastAPI и его сообществу (вам): они не только хотят предоставить вам хороший сервис, но и стремятся гарантировать, что у вас будет хороший и стабильный фреймворк — FastAPI. 🙇
+Некоторые другие облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ тоже. 🙇
Возможно, вы захотите попробовать их сервисы и воспользоваться их руководствами:
diff --git a/docs/ru/docs/deployment/fastapicloud.md b/docs/ru/docs/deployment/fastapicloud.md
new file mode 100644
index 000000000..9e7430ecb
--- /dev/null
+++ b/docs/ru/docs/deployment/fastapicloud.md
@@ -0,0 +1,65 @@
+# FastAPI Cloud { #fastapi-cloud }
+
+Вы можете развернуть своё приложение FastAPI в FastAPI Cloud одной командой, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀
+
+## Вход { #login }
+
+Убедитесь, что у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉).
+
+Затем выполните вход:
+
+
+
+```console
+$ fastapi login
+
+You are logged in to FastAPI Cloud 🚀
+```
+
+
+
+## Деплой { #deploy }
+
+Теперь разверните приложение одной командой:
+
+
+
+```console
+$ fastapi deploy
+
+Deploying to FastAPI Cloud...
+
+✅ Deployment successful!
+
+🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
+```
+
+
+
+Вот и всё! Теперь вы можете открыть своё приложение по этому URL. ✨
+
+## О FastAPI Cloud { #about-fastapi-cloud }
+
+**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**.
+
+Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
+
+Он переносит тот же **опыт разработчика**, что вы получаете при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉
+
+Он также возьмёт на себя большинство вещей, которые требуются при развертывании приложения, например:
+
+* HTTPS
+* Репликация с автоматическим масштабированием на основе запросов
+* и т.д.
+
+FastAPI Cloud — основной спонсор и источник финансирования open source‑проектов «FastAPI и друзья». ✨
+
+## Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
+
+FastAPI — проект с открытым исходным кодом и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор.
+
+Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓
+
+## Развертывание на собственном сервере { #deploy-your-own-server }
+
+Позже в этом руководстве по **развертыванию** я также расскажу все детали — чтобы вы понимали, что происходит, что нужно сделать и как развернуть приложения FastAPI самостоятельно, в том числе на собственных серверах. 🤓
diff --git a/docs/ru/docs/deployment/index.md b/docs/ru/docs/deployment/index.md
index c85fa0d52..ffb77641d 100644
--- a/docs/ru/docs/deployment/index.md
+++ b/docs/ru/docs/deployment/index.md
@@ -12,10 +12,12 @@
## Стратегии развёртывания { #deployment-strategies }
-В зависимости от вашего конкретного случая, есть несколько способов сделать это.
+Есть несколько способов сделать это, в зависимости от вашего конкретного случая и используемых вами инструментов.
Вы можете **развернуть сервер** самостоятельно, используя различные инструменты. Например, можно использовать **облачный сервис**, который выполнит часть работы за вас. Также возможны и другие варианты.
+Например, мы, команда, стоящая за FastAPI, создали **FastAPI Cloud**, чтобы сделать развёртывание приложений FastAPI в облаке как можно более простым и прямолинейным, с тем же удобством для разработчика, что и при работе с FastAPI.
+
В этом блоке я покажу вам некоторые из основных концепций, которые вы, вероятно, должны иметь в виду при развертывании приложения **FastAPI** (хотя большинство из них применимо к любому другому типу веб-приложений).
В последующих разделах вы узнаете больше деталей и методов, необходимых для этого. ✨
diff --git a/docs/ru/docs/how-to/authentication-error-status-code.md b/docs/ru/docs/how-to/authentication-error-status-code.md
new file mode 100644
index 000000000..5675cecc5
--- /dev/null
+++ b/docs/ru/docs/how-to/authentication-error-status-code.md
@@ -0,0 +1,17 @@
+# Использование старых статус-кодов ошибок аутентификации 403 { #use-old-403-authentication-error-status-codes }
+
+До версии FastAPI `0.122.0`, когда встроенные утилиты безопасности возвращали ошибку клиенту после неудачной аутентификации, они использовали HTTP статус-код `403 Forbidden`.
+
+Начиная с версии FastAPI `0.122.0`, используется более подходящий HTTP статус-код `401 Unauthorized`, и в ответе возвращается имеющий смысл HTTP-заголовок `WWW-Authenticate` в соответствии со спецификациями HTTP, RFC 7235, RFC 9110.
+
+Но если по какой-то причине ваши клиенты зависят от старого поведения, вы можете вернуть его, переопределив метод `make_not_authenticated_error` в ваших Security-классах.
+
+Например, вы можете создать подкласс `HTTPBearer`, который будет возвращать ошибку `403 Forbidden` вместо стандартной `401 Unauthorized`:
+
+{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
+
+/// tip | Совет
+
+Обратите внимание, что функция возвращает экземпляр исключения, не вызывает его. Выброс выполняется остальным внутренним кодом.
+
+///
diff --git a/docs/ru/docs/how-to/configure-swagger-ui.md b/docs/ru/docs/how-to/configure-swagger-ui.md
index 4793cc9db..9d104423d 100644
--- a/docs/ru/docs/how-to/configure-swagger-ui.md
+++ b/docs/ru/docs/how-to/configure-swagger-ui.md
@@ -40,7 +40,7 @@ FastAPI включает некоторые параметры конфигур
Это включает следующие настройки по умолчанию:
-{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *}
+{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *}
Вы можете переопределить любую из них, указав другое значение в аргументе `swagger_ui_parameters`.
diff --git a/docs/ru/docs/how-to/custom-request-and-route.md b/docs/ru/docs/how-to/custom-request-and-route.md
index 1b8d7f7ed..feef9670a 100644
--- a/docs/ru/docs/how-to/custom-request-and-route.md
+++ b/docs/ru/docs/how-to/custom-request-and-route.md
@@ -42,7 +42,7 @@
Таким образом, один и тот же класс маршрута сможет обрабатывать как gzip-сжатые, так и несжатые запросы.
-{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *}
+{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *}
### Создать пользовательский класс `GzipRoute` { #create-a-custom-gziproute-class }
@@ -54,7 +54,7 @@
Здесь мы используем её, чтобы создать `GzipRequest` из исходного HTTP-запроса.
-{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *}
+{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *}
/// note | Технические детали
@@ -92,18 +92,18 @@
Нужно лишь обработать запрос внутри блока `try`/`except`:
-{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *}
+{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *}
Если произойдёт исключение, экземпляр `Request` всё ещё будет в области видимости, поэтому мы сможем прочитать тело запроса и использовать его при обработке ошибки:
-{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *}
+{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *}
## Пользовательский класс `APIRoute` в роутере { #custom-apiroute-class-in-a-router }
Вы также можете задать параметр `route_class` у `APIRouter`:
-{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *}
+{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *}
В этом примере *операции пути*, объявленные в `router`, будут использовать пользовательский класс `TimedRoute` и получат дополнительный HTTP-заголовок `X-Response-Time` в ответе с временем, затраченным на формирование ответа:
-{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *}
+{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *}
diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md
index 75cd63223..b562cbe5b 100644
--- a/docs/ru/docs/index.md
+++ b/docs/ru/docs/index.md
@@ -52,14 +52,20 @@ FastAPI — это современный, быстрый (высокопрои
-{% if sponsors %}
+### Ключевой-спонсор { #keystone-sponsor }
+
+{% for sponsor in sponsors.keystone -%}
+
+{% endfor -%}
+
+### Золотые и серебряные спонсоры { #gold-and-silver-sponsors }
+
{% for sponsor in sponsors.gold -%}
{% endfor -%}
{%- for sponsor in sponsors.silver -%}
{% endfor %}
-{% endif %}
@@ -444,6 +450,58 @@ item: Item
* **сессии с использованием cookie**
* ...и многое другое.
+### Разверните приложение (опционально) { #deploy-your-app-optional }
+
+При желании вы можете развернуть своё приложение FastAPI в FastAPI Cloud, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀
+
+Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть ваше приложение одной командой.
+
+Перед развертыванием убедитесь, что вы вошли в систему:
+
+
+
+```console
+$ fastapi login
+
+You are logged in to FastAPI Cloud 🚀
+```
+
+
+
+Затем разверните приложение:
+
+
+
+```console
+$ fastapi deploy
+
+Deploying to FastAPI Cloud...
+
+✅ Deployment successful!
+
+🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
+```
+
+
+
+Вот и всё! Теперь вы можете открыть ваше приложение по этой ссылке. ✨
+
+#### О FastAPI Cloud { #about-fastapi-cloud }
+
+**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**.
+
+Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API при минимальных усилиях.
+
+Он переносит тот же **опыт разработчика**, что и при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉
+
+FastAPI Cloud — основной спонсор и источник финансирования для проектов с открытым исходным кодом из экосистемы *FastAPI and friends*. ✨
+
+#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
+
+FastAPI — это open source и стандартизированный фреймворк. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор.
+
+Следуйте руководствам вашего облачного провайдера по развертыванию приложений FastAPI. 🤓
+
## Производительность { #performance }
Независимые бенчмарки TechEmpower показывают приложения **FastAPI**, работающие под управлением Uvicorn, как один из самых быстрых доступных фреймворков Python, уступающий только самим Starlette и Uvicorn (используются внутри FastAPI). (*)
diff --git a/docs/ru/docs/project-generation.md b/docs/ru/docs/project-generation.md
index 8c5681115..dbedf76fe 100644
--- a/docs/ru/docs/project-generation.md
+++ b/docs/ru/docs/project-generation.md
@@ -13,8 +13,8 @@
- 🔍 [Pydantic](https://docs.pydantic.dev), используется FastAPI, для валидации данных и управления настройками.
- 💾 [PostgreSQL](https://www.postgresql.org) в качестве SQL‑базы данных.
- 🚀 [React](https://react.dev) для фронтенда.
- - 💃 Используются TypeScript, хуки, [Vite](https://vitejs.dev) и другие части современного фронтенд‑стека.
- - 🎨 [Chakra UI](https://chakra-ui.com) для компонентов фронтенда.
+ - 💃 Используются TypeScript, хуки, Vite и другие части современного фронтенд‑стека.
+ - 🎨 [Tailwind CSS](https://tailwindcss.com) и [shadcn/ui](https://ui.shadcn.com) для компонентов фронтенда.
- 🤖 Автоматически сгенерированный фронтенд‑клиент.
- 🧪 [Playwright](https://playwright.dev) для End‑to‑End тестирования.
- 🦇 Поддержка тёмной темы.
diff --git a/docs/ru/docs/resources/index.md b/docs/ru/docs/resources/index.md
index 54be4e5fd..faf80f7f4 100644
--- a/docs/ru/docs/resources/index.md
+++ b/docs/ru/docs/resources/index.md
@@ -1,3 +1,3 @@
# Ресурсы { #resources }
-Дополнительные ресурсы, внешние ссылки, статьи и многое другое. ✈️
+Дополнительные ресурсы, внешние ссылки и многое другое. ✈️
diff --git a/docs/ru/docs/tutorial/bigger-applications.md b/docs/ru/docs/tutorial/bigger-applications.md
index b832383cc..5e5d6ada9 100644
--- a/docs/ru/docs/tutorial/bigger-applications.md
+++ b/docs/ru/docs/tutorial/bigger-applications.md
@@ -85,17 +85,13 @@ from app.routers import items
Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`.
-```Python hl_lines="1 3" title="app/routers/users.py"
-{!../../docs_src/bigger_applications/app/routers/users.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *}
### Создание *эндпоинтов* с помощью `APIRouter` { #path-operations-with-apirouter }
В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`:
-```Python hl_lines="6 11 16" title="app/routers/users.py"
-{!../../docs_src/bigger_applications/app/routers/users.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}
Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`.
@@ -119,35 +115,7 @@ from app.routers import items
Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка:
-//// tab | Python 3.9+
-
-```Python hl_lines="3 6-8" title="app/dependencies.py"
-{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!}
-```
-
-////
-
-//// tab | Python 3.8+
-
-```Python hl_lines="1 5-7" title="app/dependencies.py"
-{!> ../../docs_src/bigger_applications/app_an/dependencies.py!}
-```
-
-////
-
-//// tab | Python 3.8+ non-Annotated
-
-/// tip | Подсказка
-
-Мы рекомендуем использовать версию `Annotated`, когда это возможно.
-
-///
-
-```Python hl_lines="1 4-6" title="app/dependencies.py"
-{!> ../../docs_src/bigger_applications/app/dependencies.py!}
-```
-
-////
+{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}
/// tip | Подсказка
@@ -180,9 +148,7 @@ from app.routers import items
Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*,
мы добавим их в `APIRouter`.
-```Python hl_lines="5-10 16 21" title="app/routers/items.py"
-{!../../docs_src/bigger_applications/app/routers/items.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}
Так как каждый *эндпоинт* начинается с символа `/`:
@@ -241,9 +207,7 @@ async def read_item(item_id: str):
Мы используем операцию относительного импорта `..` для импорта зависимости:
-```Python hl_lines="3" title="app/routers/items.py"
-{!../../docs_src/bigger_applications/app/routers/items.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *}
#### Как работает относительный импорт? { #how-relative-imports-work }
@@ -313,9 +277,7 @@ from ...dependencies import get_token_header
Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*:
-```Python hl_lines="30-31" title="app/routers/items.py"
-{!../../docs_src/bigger_applications/app/routers/items.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *}
/// tip | Подсказка
@@ -341,17 +303,13 @@ from ...dependencies import get_token_header
Мы даже можем объявить [глобальные зависимости](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора:
-```Python hl_lines="1 3 7" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *}
### Импорт `APIRouter` { #import-the-apirouter }
Теперь мы импортируем другие суб-модули, содержащие `APIRouter`:
-```Python hl_lines="4-5" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *}
Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`.
@@ -414,17 +372,13 @@ from .routers.users import router
Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули:
-```Python hl_lines="5" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *}
### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` { #include-the-apirouters-for-users-and-items }
Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`:
-```Python hl_lines="10-11" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *}
/// info | Примечание
@@ -465,17 +419,13 @@ from .routers.users import router
В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов,
то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`:
-```Python hl_lines="3" title="app/internal/admin.py"
-{!../../docs_src/bigger_applications/app/internal/admin.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *}
Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`).
Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`.
-```Python hl_lines="14-17" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *}
Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации.
@@ -496,9 +446,7 @@ from .routers.users import router
Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷:
-```Python hl_lines="21-23" title="app/main.py"
-{!../../docs_src/bigger_applications/app/main.py!}
-```
+{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *}
и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`.
diff --git a/docs/ru/docs/tutorial/cookie-param-models.md b/docs/ru/docs/tutorial/cookie-param-models.md
index daac764e3..182813afd 100644
--- a/docs/ru/docs/tutorial/cookie-param-models.md
+++ b/docs/ru/docs/tutorial/cookie-param-models.md
@@ -50,7 +50,7 @@
Вы можете сконфигурировать Pydantic-модель так, чтобы запретить (`forbid`) любые дополнительные (`extra`) поля:
-{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *}
+{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *}
Если клиент попробует отправить **дополнительные cookies**, то в ответ он получит **ошибку**.
diff --git a/docs/ru/docs/tutorial/first-steps.md b/docs/ru/docs/tutorial/first-steps.md
index c82118cbe..6f59d7205 100644
--- a/docs/ru/docs/tutorial/first-steps.md
+++ b/docs/ru/docs/tutorial/first-steps.md
@@ -143,6 +143,42 @@ OpenAPI определяет схему API для вашего API. И эта
Вы также можете использовать её для автоматической генерации кода для клиентов, которые взаимодействуют с вашим API. Например, для фронтенд-, мобильных или IoT-приложений.
+### Разверните приложение (необязательно) { #deploy-your-app-optional }
+
+При желании вы можете развернуть своё приложение FastAPI в FastAPI Cloud, перейдите и присоединитесь к списку ожидания, если ещё не сделали этого. 🚀
+
+Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть приложение одной командой.
+
+Перед развертыванием убедитесь, что вы вошли в систему:
+
+
+
+```console
+$ fastapi login
+
+You are logged in to FastAPI Cloud 🚀
+```
+
+
+
+Затем разверните приложение:
+
+
+
+```console
+$ fastapi deploy
+
+Deploying to FastAPI Cloud...
+
+✅ Deployment successful!
+
+🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
+```
+
+
+
+Готово! Теперь вы можете открыть своё приложение по этому URL. ✨
+
## Рассмотрим поэтапно { #recap-step-by-step }
### Шаг 1: импортируйте `FastAPI` { #step-1-import-fastapi }
@@ -314,6 +350,26 @@ https://example.com/items/foo
Многие другие объекты и модели будут автоматически преобразованы в JSON (включая ORM и т. п.). Попробуйте использовать те, что вам привычнее, с высокой вероятностью они уже поддерживаются.
+### Шаг 6: разверните приложение { #step-6-deploy-it }
+
+Разверните приложение в **FastAPI Cloud** одной командой: `fastapi deploy`. 🎉
+
+#### О FastAPI Cloud { #about-fastapi-cloud }
+
+**FastAPI Cloud** создан тем же автором и командой, что и **FastAPI**.
+
+Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
+
+Он переносит тот же **опыт разработчика** при создании приложений с FastAPI на их **развертывание** в облаке. 🎉
+
+FastAPI Cloud — основной спонсор и источник финансирования для open-source проектов «FastAPI и друзья». ✨
+
+#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
+
+FastAPI — open-source и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера по вашему выбору.
+
+Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓
+
## Резюме { #recap }
* Импортируйте `FastAPI`.
@@ -321,3 +377,4 @@ https://example.com/items/foo
* Напишите **декоратор операции пути**, например `@app.get("/")`.
* Определите **функцию операции пути**; например, `def root(): ...`.
* Запустите сервер разработки командой `fastapi dev`.
+* При желании разверните приложение командой `fastapi deploy`.
diff --git a/docs/ru/docs/tutorial/handling-errors.md b/docs/ru/docs/tutorial/handling-errors.md
index 2378c8b04..63ca8665e 100644
--- a/docs/ru/docs/tutorial/handling-errors.md
+++ b/docs/ru/docs/tutorial/handling-errors.md
@@ -81,7 +81,7 @@
## Установка пользовательских обработчиков исключений { #install-custom-exception-handlers }
-Вы можете добавить пользовательские обработчики исключений с помощью то же самое исключение - утилиты от Starlette.
+Вы можете добавить пользовательские обработчики исключений с помощью тех же утилит обработки исключений из Starlette.
Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`.
@@ -117,7 +117,7 @@
Вы можете переопределить эти обработчики исключений на свои собственные.
-### Переопределение исключений проверки запроса { #override-request-validation-exceptions }
+### Переопределение обработчика исключений проверки запроса { #override-request-validation-exceptions }
Когда запрос содержит недопустимые данные, **FastAPI** внутренне вызывает ошибку `RequestValidationError`.
@@ -127,7 +127,7 @@
Обработчик исключения получит объект `Request` и исключение.
-{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *}
+{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *}
Теперь, если перейти к `/items/foo`, то вместо стандартной JSON-ошибки с:
@@ -149,36 +149,17 @@
вы получите текстовую версию:
```
-1 validation error
-path -> item_id
- value is not a valid integer (type=type_error.integer)
+Validation errors:
+Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
```
-#### `RequestValidationError` или `ValidationError` { #requestvalidationerror-vs-validationerror }
-
-/// warning | Внимание
-
-Это технические детали, которые можно пропустить, если они не важны для вас сейчас.
-
-///
-
-`RequestValidationError` является подклассом Pydantic `ValidationError`.
-
-**FastAPI** использует его для того, чтобы, если вы используете Pydantic-модель в `response_model`, и ваши данные содержат ошибку, вы увидели ошибку в журнале.
-
-Но клиент/пользователь этого не увидит. Вместо этого клиент получит сообщение "Internal Server Error" с кодом состояния HTTP `500`.
-
-Так и должно быть, потому что если в вашем *ответе* или где-либо в вашем коде (не в *запросе* клиента) возникает Pydantic `ValidationError`, то это действительно ошибка в вашем коде.
-
-И пока вы не устраните ошибку, ваши клиенты/пользователи не должны иметь доступа к внутренней информации о ней, так как это может привести к уязвимости в системе безопасности.
-
### Переопределите обработчик ошибок `HTTPException` { #override-the-httpexception-error-handler }
Аналогичным образом можно переопределить обработчик `HTTPException`.
Например, для этих ошибок можно вернуть обычный текстовый ответ вместо JSON:
-{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *}
+{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,25] *}
/// note | Технические детали
@@ -188,6 +169,14 @@ path -> item_id
///
+/// warning | Внимание
+
+Имейте в виду, что `RequestValidationError` содержит информацию об имени файла и строке, где произошла ошибка валидации, чтобы вы могли при желании отобразить её в логах с релевантными данными.
+
+Но это означает, что если вы просто преобразуете её в строку и вернёте эту информацию напрямую, вы можете допустить небольшую утечку информации о своей системе, поэтому здесь код извлекает и показывает каждую ошибку отдельно.
+
+///
+
### Используйте тело `RequestValidationError` { #use-the-requestvalidationerror-body }
Ошибка `RequestValidationError` содержит полученное `тело` с недопустимыми данными.
diff --git a/docs/ru/docs/tutorial/security/index.md b/docs/ru/docs/tutorial/security/index.md
index 8fb4bf24f..ebac013b6 100644
--- a/docs/ru/docs/tutorial/security/index.md
+++ b/docs/ru/docs/tutorial/security/index.md
@@ -1,4 +1,4 @@
-# Настройка авторизации
+# Настройка авторизации { #security }
Существует множество способов обеспечения безопасности, аутентификации и авторизации.
@@ -10,11 +10,11 @@
Но сначала давайте рассмотрим некоторые небольшие концепции.
-## Куда-то торопишься?
+## Куда-то торопишься? { #in-a-hurry }
Если вам не нужна информация о каких-либо из следующих терминов и вам просто нужно добавить защиту с аутентификацией на основе логина и пароля *прямо сейчас*, переходите к следующим главам.
-## OAuth2
+## OAuth2 { #oauth2 }
OAuth2 - это протокол, который определяет несколько способов обработки аутентификации и авторизации.
@@ -24,7 +24,7 @@ OAuth2 включает в себя способы аутентификации
Это то, что используют под собой все кнопки "вход с помощью Facebook, Google, X (Twitter), GitHub" на страницах авторизации.
-### OAuth 1
+### OAuth 1 { #oauth-1 }
Ранее использовался протокол OAuth 1, который сильно отличается от OAuth2 и является более сложным, поскольку он включал прямые описания того, как шифровать сообщение.
@@ -34,11 +34,11 @@ OAuth2 не указывает, как шифровать сообщение, о
/// tip | Подсказка
-В разделе **Развертывание** вы увидите [как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt.](https://fastapi.tiangolo.com/ru/deployment/https/)
+В разделе **Развертывание** вы увидите как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt.
///
-## OpenID Connect
+## OpenID Connect { #openid-connect }
OpenID Connect - это еще один протокол, основанный на **OAuth2**.
@@ -48,7 +48,7 @@ OpenID Connect - это еще один протокол, основанный
Но вход в Facebook не поддерживает OpenID Connect. У него есть собственная вариация OAuth2.
-### OpenID (не "OpenID Connect")
+### OpenID (не "OpenID Connect") { #openid-not-openid-connect }
Также ранее использовался стандарт "OpenID", который пытался решить ту же проблему, что и **OpenID Connect**, но не был основан на OAuth2.
@@ -56,7 +56,7 @@ OpenID Connect - это еще один протокол, основанный
В настоящее время не очень популярен и не используется.
-## OpenAPI
+## OpenAPI { #openapi }
OpenAPI (ранее известный как Swagger) - это открытая спецификация для создания API (в настоящее время является частью Linux Foundation).
@@ -97,7 +97,7 @@ OpenAPI может использовать следующие схемы авт
///
-## Преимущества **FastAPI**
+## Преимущества **FastAPI** { #fastapi-utilities }
Fast API предоставляет несколько инструментов для каждой из этих схем безопасности в модуле `fastapi.security`, которые упрощают использование этих механизмов безопасности.
diff --git a/docs/ru/docs/tutorial/sql-databases.md b/docs/ru/docs/tutorial/sql-databases.md
index c44f37b9a..1d0346533 100644
--- a/docs/ru/docs/tutorial/sql-databases.md
+++ b/docs/ru/docs/tutorial/sql-databases.md
@@ -63,9 +63,9 @@ $ pip install sqlmodel
* `table=True` сообщает SQLModel, что это *модель-таблица*, она должна представлять **таблицу** в SQL базе данных, это не просто *модель данных* (как обычный класс Pydantic).
-* `Field(primary_key=True)` сообщает SQLModel, что `id` — это **первичный ключ** в SQL базе данных (подробнее о первичных ключах можно узнать в документации SQLModel).
+* `Field(primary_key=True)` сообщает SQLModel, что `id` — это **первичный ключ** в SQL базе данных (подробнее о первичных ключах SQL можно узнать в документации SQLModel).
- Благодаря типу `int | None`, SQLModel будет знать, что этот столбец должен быть `INTEGER` в SQL базе данных и должен допускать значение `NULL`.
+ **Примечание:** Мы используем `int | None` для поля первичного ключа, чтобы в Python-коде можно было *создать объект без `id`* (`id=None`), предполагая, что база данных *сгенерирует его при сохранении*. SQLModel понимает, что база данных предоставит `id`, и *определяет столбец как `INTEGER` (не `NULL`)* в схеме базы данных. См. документацию SQLModel о первичных ключах для подробностей.
* `Field(index=True)` сообщает SQLModel, что нужно создать **SQL индекс** для этого столбца, что позволит быстрее выполнять выборки при чтении данных, отфильтрованных по этому столбцу.
@@ -107,7 +107,7 @@ $ pip install sqlmodel
Здесь мы создаём таблицы в обработчике события запуска приложения.
-Для продакшна вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓
+Для продакшн вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓
/// tip | Подсказка
diff --git a/docs/ru/docs/tutorial/testing.md b/docs/ru/docs/tutorial/testing.md
index 0224798b1..7354ed895 100644
--- a/docs/ru/docs/tutorial/testing.md
+++ b/docs/ru/docs/tutorial/testing.md
@@ -121,63 +121,13 @@ $ pip install httpx
Обе *операции пути* требуют наличия в запросе заголовка `X-Token`.
-//// tab | Python 3.10+
-
-```Python
-{!> ../../docs_src/app_testing/app_b_an_py310/main.py!}
-```
-
-////
-
-//// tab | Python 3.9+
-
-```Python
-{!> ../../docs_src/app_testing/app_b_an_py39/main.py!}
-```
-
-////
-
-//// tab | Python 3.8+
-
-```Python
-{!> ../../docs_src/app_testing/app_b_an/main.py!}
-```
-
-////
-
-//// tab | Python 3.10+ без Annotated
-
-/// tip | Подсказка
-
-По возможности используйте версию с `Annotated`.
-
-///
-
-```Python
-{!> ../../docs_src/app_testing/app_b_py310/main.py!}
-```
-
-////
-
-//// tab | Python 3.8+ без Annotated
-
-/// tip | Подсказка
-
-По возможности используйте версию с `Annotated`.
-
-///
-
-```Python
-{!> ../../docs_src/app_testing/app_b/main.py!}
-```
-
-////
+{* ../../docs_src/app_testing/app_b_an_py310/main.py *}
### Расширенный файл тестов { #extended-testing-file }
Теперь обновим файл `test_main.py`, добавив в него тестов:
-{* ../../docs_src/app_testing/app_b/test_main.py *}
+{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *}
Если Вы не знаете, как передать информацию в запросе, можете воспользоваться поисковиком (погуглить) и задать вопрос: "Как передать информацию в запросе с помощью `httpx`", можно даже спросить: "Как передать информацию в запросе с помощью `requests`", поскольку дизайн HTTPX основан на дизайне Requests.
diff --git a/docs/ru/docs/virtual-environments.md b/docs/ru/docs/virtual-environments.md
index 5153cd486..43136298a 100644
--- a/docs/ru/docs/virtual-environments.md
+++ b/docs/ru/docs/virtual-environments.md
@@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip
+/// tip | Подсказка
+
+Иногда при попытке обновить pip вы можете получить ошибку **`No module named pip`**.
+
+Если это произошло, установите и обновите pip с помощью команды ниже:
+
+
+
+```console
+$ python -m ensurepip --upgrade
+
+---> 100%
+```
+
+
+
+Эта команда установит pip, если он ещё не установлен, а также гарантирует, что установленная версия pip будет не старее, чем версия, доступная в `ensurepip`.
+
+///
+
## Добавление `.gitignore` { #add-gitignore }
Если вы используете **Git** (а вам стоит его использовать), добавьте файл `.gitignore`, чтобы исключить из Git всё, что находится в вашей `.venv`.
@@ -834,7 +854,7 @@ I solemnly swear 🐺
* Управлять **виртуальным окружением** ваших проектов
* Устанавливать **пакеты**
* Управлять **зависимостями и версиями** пакетов вашего проекта
-* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшне точно так же, как и на компьютере при разработке — это называется **locking**
+* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшн точно так же, как и на компьютере при разработке — это называется **locking**
* И многое другое
## Заключение { #conclusion }
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index 8de426ad4..e02969c55 100644
--- a/fastapi/__init__.py
+++ b/fastapi/__init__.py
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
-__version__ = "0.124.2"
+__version__ = "0.124.4"
from starlette import status as status
diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py
index b5e6e8983..97a48bf86 100644
--- a/fastapi/_compat/v2.py
+++ b/fastapi/_compat/v2.py
@@ -18,7 +18,7 @@ from typing import (
from fastapi._compat import may_v1, shared
from fastapi.openapi.constants import REF_TEMPLATE
from fastapi.types import IncEx, ModelNameMap, UnionType
-from pydantic import BaseModel, ConfigDict, TypeAdapter, create_model
+from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
from pydantic import ValidationError as ValidationError
@@ -50,6 +50,45 @@ UndefinedType = PydanticUndefinedType
evaluate_forwardref = eval_type_lenient
Validator = Any
+# TODO: remove when dropping support for Pydantic < v2.12.3
+_Attrs = {
+ "default": ...,
+ "default_factory": None,
+ "alias": None,
+ "alias_priority": None,
+ "validation_alias": None,
+ "serialization_alias": None,
+ "title": None,
+ "field_title_generator": None,
+ "description": None,
+ "examples": None,
+ "exclude": None,
+ "exclude_if": None,
+ "discriminator": None,
+ "deprecated": None,
+ "json_schema_extra": None,
+ "frozen": None,
+ "validate_default": None,
+ "repr": True,
+ "init": None,
+ "init_var": None,
+ "kw_only": None,
+}
+
+
+# TODO: remove when dropping support for Pydantic < v2.12.3
+def asdict(field_info: FieldInfo) -> Dict[str, Any]:
+ attributes = {}
+ for attr in _Attrs:
+ value = getattr(field_info, attr, Undefined)
+ if value is not Undefined:
+ attributes[attr] = value
+ return {
+ "annotation": field_info.annotation,
+ "metadata": field_info.metadata,
+ "attributes": attributes,
+ }
+
class BaseConfig:
pass
@@ -71,6 +110,18 @@ class ModelField:
a = self.field_info.alias
return a if a is not None else self.name
+ @property
+ def validation_alias(self) -> Union[str, None]:
+ va = self.field_info.validation_alias
+ if isinstance(va, str) and va:
+ return va
+ return None
+
+ @property
+ def serialization_alias(self) -> Union[str, None]:
+ sa = self.field_info.serialization_alias
+ return sa or None
+
@property
def required(self) -> bool:
return self.field_info.is_required()
@@ -95,10 +146,15 @@ class ModelField:
warnings.simplefilter(
"ignore", category=UnsupportedFieldAttributeWarning
)
+ # TODO: remove after dropping support for Python 3.8 and
+ # setting the min Pydantic to v2.12.3 that adds asdict()
+ field_dict = asdict(self.field_info)
annotated_args = (
- self.field_info.annotation,
- *self.field_info.metadata,
- self.field_info,
+ field_dict["annotation"],
+ *field_dict["metadata"],
+ # this FieldInfo needs to be created again so that it doesn't include
+ # the old field info metadata and only the rest of the attributes
+ Field(**field_dict["attributes"]),
)
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[annotated_args],
@@ -207,12 +263,17 @@ def get_schema_from_model_field(
if (separate_input_output_schemas or _has_computed_fields(field))
else "validation"
)
+ field_alias = (
+ (field.validation_alias or field.alias)
+ if field.mode == "validation"
+ else (field.serialization_alias or field.alias)
+ )
# This expects that GenerateJsonSchema was already used to generate the definitions
json_schema = field_mapping[(field, override_mode or field.mode)]
if "$ref" not in json_schema:
# TODO remove when deprecating Pydantic v1
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
- json_schema["title"] = field.field_info.title or field.alias.title().replace(
+ json_schema["title"] = field.field_info.title or field_alias.title().replace(
"_", " "
)
return json_schema
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 262dba6fd..cc7e55b4b 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -752,7 +752,7 @@ def _validate_value_with_model_field(
def _get_multidict_value(
field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None
) -> Any:
- alias = alias or field.alias
+ alias = alias or get_validation_alias(field)
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
value = values.getlist(alias)
else:
@@ -809,15 +809,13 @@ def request_params_to_args(
field.field_info, "convert_underscores", default_convert_underscores
)
if convert_underscores:
- alias = (
- field.alias
- if field.alias != field.name
- else field.name.replace("_", "-")
- )
+ alias = get_validation_alias(field)
+ if alias == field.name:
+ alias = alias.replace("_", "-")
value = _get_multidict_value(field, received_params, alias=alias)
if value is not None:
- params_to_process[field.alias] = value
- processed_keys.add(alias or field.alias)
+ params_to_process[get_validation_alias(field)] = value
+ processed_keys.add(alias or get_validation_alias(field))
for key in received_params.keys():
if key not in processed_keys:
@@ -847,7 +845,7 @@ def request_params_to_args(
assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
"Params must be subclasses of Param"
)
- loc = (field_info.in_.value, field.alias)
+ loc = (field_info.in_.value, get_validation_alias(field))
v_, errors_ = _validate_value_with_model_field(
field=field, value=value, values=values, loc=loc
)
@@ -936,8 +934,8 @@ async def _extract_form_body(
tg.start_soon(process_fn, sub_value.read)
value = serialize_sequence_value(field=field, value=results)
if value is not None:
- values[field.alias] = value
- field_aliases = {field.alias for field in body_fields}
+ values[get_validation_alias(field)] = value
+ field_aliases = {get_validation_alias(field) for field in body_fields}
for key in received_body.keys():
if key not in field_aliases:
param_values = received_body.getlist(key)
@@ -979,11 +977,11 @@ async def request_body_to_args(
)
return {first_field.name: v_}, errors_
for field in body_fields:
- loc = ("body", field.alias)
+ loc = ("body", get_validation_alias(field))
value: Optional[Any] = None
if body_to_process is not None:
try:
- value = body_to_process.get(field.alias)
+ value = body_to_process.get(get_validation_alias(field))
# If the received body is a list, not a dict
except AttributeError:
errors.append(get_missing_field_error(loc))
@@ -1062,3 +1060,8 @@ def get_body_field(
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
)
return final_field
+
+
+def get_validation_alias(field: ModelField) -> str:
+ va = getattr(field, "validation_alias", None)
+ return va or field.alias
diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py
index 06c14861a..9fe2044f2 100644
--- a/fastapi/openapi/utils.py
+++ b/fastapi/openapi/utils.py
@@ -19,6 +19,7 @@ from fastapi.dependencies.utils import (
_get_flat_fields_from_params,
get_flat_dependant,
get_flat_params,
+ get_validation_alias,
)
from fastapi.encoders import jsonable_encoder
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
@@ -141,7 +142,7 @@ def _get_openapi_operation_parameters(
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
)
- name = param.alias
+ name = get_validation_alias(param)
convert_underscores = getattr(
param.field_info,
"convert_underscores",
@@ -149,7 +150,7 @@ def _get_openapi_operation_parameters(
)
if (
param_type == ParamTypes.header
- and param.alias == param.name
+ and name == param.name
and convert_underscores
):
name = param.name.replace("_", "-")
diff --git a/fastapi/params.py b/fastapi/params.py
index 6d07df35e..b6d0f08e3 100644
--- a/fastapi/params.py
+++ b/fastapi/params.py
@@ -115,6 +115,10 @@ class Param(FieldInfo): # type: ignore[misc]
else:
kwargs["deprecated"] = deprecated
if PYDANTIC_V2:
+ if serialization_alias in (_Unset, None) and isinstance(alias, str):
+ serialization_alias = alias
+ if validation_alias in (_Unset, None):
+ validation_alias = alias
kwargs.update(
{
"annotation": annotation,
@@ -571,6 +575,10 @@ class Body(FieldInfo): # type: ignore[misc]
else:
kwargs["deprecated"] = deprecated
if PYDANTIC_V2:
+ if serialization_alias in (_Unset, None) and isinstance(alias, str):
+ serialization_alias = alias
+ if validation_alias in (_Unset, None):
+ validation_alias = alias
kwargs.update(
{
"annotation": annotation,
diff --git a/pyproject.toml b/pyproject.toml
index f8d5fa7c7..ef4440b1b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -196,6 +196,7 @@ source = [
"tests",
"fastapi"
]
+relative_files = true
context = '${CONTEXT}'
dynamic_context = "test_function"
omit = [
diff --git a/tests/test_request_params/__init__.py b/tests/test_request_params/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_request_params/test_body/__init__.py b/tests/test_request_params/test_body/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py
new file mode 100644
index 000000000..18a2a2f30
--- /dev/null
+++ b/tests/test_request_params/test_body/test_list.py
@@ -0,0 +1,483 @@
+from typing import List, Union
+
+import pytest
+from dirty_equals import IsDict, IsOneOf, IsPartialDict
+from fastapi import Body, FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/required-list-str", operation_id="required_list_str")
+async def read_required_list_str(p: Annotated[List[str], Body(embed=True)]):
+ return {"p": p}
+
+
+class BodyModelRequiredListStr(BaseModel):
+ p: List[str]
+
+
+@app.post("/model-required-list-str", operation_id="model_required_list_str")
+def read_model_required_list_str(p: BodyModelRequiredListStr):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p": {
+ "items": {"type": "string"},
+ "title": "P",
+ "type": "array",
+ },
+ },
+ "required": ["p"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize("json", [None, {}])
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str_missing(path: str, json: Union[dict, None]):
+ client = TestClient(app)
+ response = client.post(path, json=json)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": IsOneOf(["body", "p"], ["body"]),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ {
+ "detail": [
+ {
+ "loc": IsOneOf(["body", "p"], ["body"]),
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/required-list-alias", operation_id="required_list_alias")
+async def read_required_list_alias(
+ p: Annotated[List[str], Body(embed=True, alias="p_alias")],
+):
+ return {"p": p}
+
+
+class BodyModelRequiredListAlias(BaseModel):
+ p: List[str] = Field(alias="p_alias")
+
+
+@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
+async def read_model_required_list_alias(p: BodyModelRequiredListAlias):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias",
+ "/model-required-list-alias",
+ ],
+)
+def test_required_list_str_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_alias": {
+ "items": {"type": "string"},
+ "title": "P Alias",
+ "type": "array",
+ },
+ },
+ "required": ["p_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize("json", [None, {}])
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_alias_missing(path: str, json: Union[dict, None]):
+ client = TestClient(app)
+ response = client.post(path, json=json)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": IsOneOf(["body", "p_alias"], ["body"]),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": IsOneOf(["body", "p_alias"], ["body"]),
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": ["hello", "world"]})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": ["hello", "world"]}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post(
+ "/required-list-validation-alias", operation_id="required_list_validation_alias"
+)
+def read_required_list_validation_alias(
+ p: Annotated[List[str], Body(embed=True, validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class BodyModelRequiredListValidationAlias(BaseModel):
+ p: List[str] = Field(validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-required-list-validation-alias",
+ operation_id="model_required_list_validation_alias",
+)
+async def read_model_required_list_validation_alias(
+ p: BodyModelRequiredListValidationAlias,
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-validation-alias", "/model-required-list-validation-alias"],
+)
+def test_required_list_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {
+ "items": {"type": "string"},
+ "title": "P Val Alias",
+ "type": "array",
+ },
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize("json", [None, {}])
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]):
+ client = TestClient(app)
+ response = client.post(path, json=json)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": IsOneOf(["body"], ["body", "p_val_alias"]),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": ["hello", "world"]})
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_val_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/required-list-alias-and-validation-alias",
+ operation_id="required_list_alias_and_validation_alias",
+)
+def read_required_list_alias_and_validation_alias(
+ p: Annotated[
+ List[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
+ ],
+):
+ return {"p": p}
+
+
+class BodyModelRequiredListAliasAndValidationAlias(BaseModel):
+ p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-required-list-alias-and-validation-alias",
+ operation_id="model_required_list_alias_and_validation_alias",
+)
+def read_model_required_list_alias_and_validation_alias(
+ p: BodyModelRequiredListAliasAndValidationAlias,
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {
+ "items": {"type": "string"},
+ "title": "P Val Alias",
+ "type": "array",
+ },
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize("json", [None, {}])
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_missing(path: str, json):
+ client = TestClient(app)
+ response = client.post(path, json=json)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": IsOneOf(["body"], ["body", "p_val_alias"]),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": ["hello", "world"]})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": ["hello", "world"]}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_alias": ["hello", "world"]})
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_val_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
diff --git a/tests/test_request_params/test_body/test_optional_list.py b/tests/test_request_params/test_body/test_optional_list.py
new file mode 100644
index 000000000..e2ecdc7f4
--- /dev/null
+++ b/tests/test_request_params/test_body/test_optional_list.py
@@ -0,0 +1,575 @@
+from typing import List, Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import Body, FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/optional-list-str", operation_id="optional_list_str")
+async def read_optional_list_str(
+ p: Annotated[Optional[List[str]], Body(embed=True)] = None,
+):
+ return {"p": p}
+
+
+class BodyModelOptionalListStr(BaseModel):
+ p: Optional[List[str]] = None
+
+
+@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
+async def read_model_optional_list_str(p: BodyModelOptionalListStr):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p": {"items": {"type": "string"}, "type": "array", "title": "P"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+def test_optional_list_str_missing():
+ client = TestClient(app)
+ response = client.post("/optional-list-str")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+def test_model_optional_list_str_missing():
+ client = TestClient(app)
+ response = client.post("/model-optional-list-str")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "input": None,
+ "loc": ["body"],
+ "msg": "Field required",
+ "type": "missing",
+ },
+ ],
+ }
+ ) | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ],
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str_missing_empty_dict(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/optional-list-alias", operation_id="optional_list_alias")
+async def read_optional_list_alias(
+ p: Annotated[Optional[List[str]], Body(embed=True, alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class BodyModelOptionalListAlias(BaseModel):
+ p: Optional[List[str]] = Field(None, alias="p_alias")
+
+
+@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
+async def read_model_optional_list_alias(p: BodyModelOptionalListAlias):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias",
+ "/model-optional-list-alias",
+ ],
+)
+def test_optional_list_str_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_alias": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_alias": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "P Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+def test_optional_list_alias_missing():
+ client = TestClient(app)
+ response = client.post("/optional-list-alias")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+def test_model_optional_list_alias_missing():
+ client = TestClient(app)
+ response = client.post("/model-optional-list-alias")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "input": None,
+ "loc": ["body"],
+ "msg": "Field required",
+ "type": "missing",
+ },
+ ],
+ }
+ ) | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ],
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_missing_empty_dict(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_alias": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post(
+ "/optional-list-validation-alias", operation_id="optional_list_validation_alias"
+)
+def read_optional_list_validation_alias(
+ p: Annotated[
+ Optional[List[str]], Body(embed=True, validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class BodyModelOptionalListValidationAlias(BaseModel):
+ p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-optional-list-validation-alias",
+ operation_id="model_optional_list_validation_alias",
+)
+def read_model_optional_list_validation_alias(
+ p: BodyModelOptionalListValidationAlias,
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_val_alias": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_val_alias": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+def test_optional_list_validation_alias_missing():
+ client = TestClient(app)
+ response = client.post("/optional-list-validation-alias")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+def test_model_optional_list_validation_alias_missing():
+ client = TestClient(app)
+ response = client.post("/model-optional-list-validation-alias")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "input": None,
+ "loc": ["body"],
+ "msg": "Field required",
+ "type": "missing",
+ },
+ ],
+ }
+ ) | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ],
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_missing_empty_dict(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-validation-alias",
+ "/model-optional-list-validation-alias",
+ ],
+)
+def test_optional_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-validation-alias",
+ "/model-optional-list-validation-alias",
+ ],
+)
+def test_optional_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_val_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/optional-list-alias-and-validation-alias",
+ operation_id="optional_list_alias_and_validation_alias",
+)
+def read_optional_list_alias_and_validation_alias(
+ p: Annotated[
+ Optional[List[str]],
+ Body(embed=True, alias="p_alias", validation_alias="p_val_alias"),
+ ] = None,
+):
+ return {"p": p}
+
+
+class BodyModelOptionalListAliasAndValidationAlias(BaseModel):
+ p: Optional[List[str]] = Field(
+ None, alias="p_alias", validation_alias="p_val_alias"
+ )
+
+
+@app.post(
+ "/model-optional-list-alias-and-validation-alias",
+ operation_id="model_optional_list_alias_and_validation_alias",
+)
+def read_model_optional_list_alias_and_validation_alias(
+ p: BodyModelOptionalListAliasAndValidationAlias,
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_val_alias": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_val_alias": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+def test_optional_list_alias_and_validation_alias_missing():
+ client = TestClient(app)
+ response = client.post("/optional-list-alias-and-validation-alias")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+def test_model_optional_list_alias_and_validation_alias_missing():
+ client = TestClient(app)
+ response = client.post("/model-optional-list-alias-and-validation-alias")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "input": None,
+ "loc": ["body"],
+ "msg": "Field required",
+ "type": "missing",
+ },
+ ],
+ }
+ ) | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ],
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_alias": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_val_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "p": [
+ "hello",
+ "world",
+ ]
+ }
diff --git a/tests/test_request_params/test_body/test_optional_str.py b/tests/test_request_params/test_body/test_optional_str.py
new file mode 100644
index 000000000..a49f5a367
--- /dev/null
+++ b/tests/test_request_params/test_body/test_optional_str.py
@@ -0,0 +1,544 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import Body, FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/optional-str", operation_id="optional_str")
+async def read_optional_str(p: Annotated[Optional[str], Body(embed=True)] = None):
+ return {"p": p}
+
+
+class BodyModelOptionalStr(BaseModel):
+ p: Optional[str] = None
+
+
+@app.post("/model-optional-str", operation_id="model_optional_str")
+async def read_model_optional_str(p: BodyModelOptionalStr):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p": {"type": "string", "title": "P"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+def test_optional_str_missing():
+ client = TestClient(app)
+ response = client.post("/optional-str")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+def test_model_optional_str_missing():
+ client = TestClient(app)
+ response = client.post("/model-optional-str")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "input": None,
+ "loc": ["body"],
+ "msg": "Field required",
+ "type": "missing",
+ },
+ ],
+ }
+ ) | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ],
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_missing_empty_dict(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/optional-alias", operation_id="optional_alias")
+async def read_optional_alias(
+ p: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class BodyModelOptionalAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias")
+
+
+@app.post("/model-optional-alias", operation_id="model_optional_alias")
+async def read_model_optional_alias(p: BodyModelOptionalAlias):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias",
+ "/model-optional-alias",
+ ],
+)
+def test_optional_str_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_alias": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_alias": {"type": "string", "title": "P Alias"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+def test_optional_alias_missing():
+ client = TestClient(app)
+ response = client.post("/optional-alias")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+def test_model_optional_alias_missing():
+ client = TestClient(app)
+ response = client.post("/model-optional-alias")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "input": None,
+ "loc": ["body"],
+ "msg": "Field required",
+ "type": "missing",
+ },
+ ],
+ }
+ ) | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ],
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_model_optional_alias_missing_empty_dict(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
+def read_optional_validation_alias(
+ p: Annotated[
+ Optional[str], Body(embed=True, validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class BodyModelOptionalValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-optional-validation-alias", operation_id="model_optional_validation_alias"
+)
+def read_model_optional_validation_alias(
+ p: BodyModelOptionalValidationAlias,
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_val_alias": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_val_alias": {"type": "string", "title": "P Val Alias"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@needs_pydanticv2
+def test_optional_validation_alias_missing():
+ client = TestClient(app)
+ response = client.post("/optional-validation-alias")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+def test_model_optional_validation_alias_missing():
+ client = TestClient(app)
+ response = client.post("/model-optional-validation-alias")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "input": None,
+ "loc": ["body"],
+ "msg": "Field required",
+ "type": "missing",
+ },
+ ],
+ }
+ ) | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ],
+ }
+ )
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_model_optional_validation_alias_missing_empty_dict(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_val_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/optional-alias-and-validation-alias",
+ operation_id="optional_alias_and_validation_alias",
+)
+def read_optional_alias_and_validation_alias(
+ p: Annotated[
+ Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class BodyModelOptionalAliasAndValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-optional-alias-and-validation-alias",
+ operation_id="model_optional_alias_and_validation_alias",
+)
+def read_model_optional_alias_and_validation_alias(
+ p: BodyModelOptionalAliasAndValidationAlias,
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_val_alias": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_val_alias": {"type": "string", "title": "P Val Alias"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@needs_pydanticv2
+def test_optional_alias_and_validation_alias_missing():
+ client = TestClient(app)
+ response = client.post("/optional-alias-and-validation-alias")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+def test_model_optional_alias_and_validation_alias_missing():
+ client = TestClient(app)
+ response = client.post("/model-optional-alias-and-validation-alias")
+ assert response.status_code == 422, response.text
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "input": None,
+ "loc": ["body"],
+ "msg": "Field required",
+ "type": "missing",
+ },
+ ],
+ }
+ ) | IsDict(
+ {
+ # TODO: remove when deprecating Pydantic v1
+ "detail": [
+ {
+ "loc": ["body"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ },
+ ],
+ }
+ )
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_model_optional_alias_and_validation_alias_missing_empty_dict(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_val_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_body/test_required_str.py b/tests/test_request_params/test_body/test_required_str.py
new file mode 100644
index 000000000..18c660cad
--- /dev/null
+++ b/tests/test_request_params/test_body/test_required_str.py
@@ -0,0 +1,472 @@
+from typing import Any, Dict, Union
+
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import Body, FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/required-str", operation_id="required_str")
+async def read_required_str(p: Annotated[str, Body(embed=True)]):
+ return {"p": p}
+
+
+class BodyModelRequiredStr(BaseModel):
+ p: str
+
+
+@app.post("/model-required-str", operation_id="model_required_str")
+async def read_model_required_str(p: BodyModelRequiredStr):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p": {"title": "P", "type": "string"},
+ },
+ "required": ["p"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize("json", [None, {}])
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_missing(path: str, json: Union[Dict[str, Any], None]):
+ client = TestClient(app)
+ response = client.post(path, json=json)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": IsOneOf(["body"], ["body", "p"]),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": IsOneOf(["body"], ["body", "p"]),
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/required-alias", operation_id="required_alias")
+async def read_required_alias(
+ p: Annotated[str, Body(embed=True, alias="p_alias")],
+):
+ return {"p": p}
+
+
+class BodyModelRequiredAlias(BaseModel):
+ p: str = Field(alias="p_alias")
+
+
+@app.post("/model-required-alias", operation_id="model_required_alias")
+async def read_model_required_alias(p: BodyModelRequiredAlias):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias",
+ "/model-required-alias",
+ ],
+)
+def test_required_str_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_alias": {"title": "P Alias", "type": "string"},
+ },
+ "required": ["p_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize("json", [None, {}])
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_missing(path: str, json: Union[Dict[str, Any], None]):
+ client = TestClient(app)
+ response = client.post(path, json=json)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": IsOneOf(["body", "p_alias"], ["body"]),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": IsOneOf(["body", "p_alias"], ["body"]),
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": "hello"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": IsOneOf(["body", "p_alias"], ["body"]),
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_alias": "hello"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post("/required-validation-alias", operation_id="required_validation_alias")
+def read_required_validation_alias(
+ p: Annotated[str, Body(embed=True, validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class BodyModelRequiredValidationAlias(BaseModel):
+ p: str = Field(validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-required-validation-alias", operation_id="model_required_validation_alias"
+)
+def read_model_required_validation_alias(
+ p: BodyModelRequiredValidationAlias,
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-validation-alias", "/model-required-validation-alias"],
+)
+def test_required_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {"title": "P Val Alias", "type": "string"},
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize("json", [None, {}])
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_missing(
+ path: str, json: Union[Dict[str, Any], None]
+):
+ client = TestClient(app)
+ response = client.post(path, json=json)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": IsOneOf(["body", "p_val_alias"], ["body"]),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": "hello"})
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_val_alias": "hello"})
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/required-alias-and-validation-alias",
+ operation_id="required_alias_and_validation_alias",
+)
+def read_required_alias_and_validation_alias(
+ p: Annotated[
+ str, Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
+ ],
+):
+ return {"p": p}
+
+
+class BodyModelRequiredAliasAndValidationAlias(BaseModel):
+ p: str = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-required-alias-and-validation-alias",
+ operation_id="model_required_alias_and_validation_alias",
+)
+def read_model_required_alias_and_validation_alias(
+ p: BodyModelRequiredAliasAndValidationAlias,
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {"title": "P Val Alias", "type": "string"},
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize("json", [None, {}])
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_missing(
+ path: str, json: Union[Dict[str, Any], None]
+):
+ client = TestClient(app)
+ response = client.post(path, json=json)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": IsOneOf(["body"], ["body", "p_val_alias"]),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p": "hello"})
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_alias": "hello"})
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p_alias": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, json={"p_val_alias": "hello"})
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_body/utils.py b/tests/test_request_params/test_body/utils.py
new file mode 100644
index 000000000..5151a82d3
--- /dev/null
+++ b/tests/test_request_params/test_body/utils.py
@@ -0,0 +1,7 @@
+from typing import Any, Dict
+
+
+def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
+ body = openapi["paths"][path]["post"]["requestBody"]
+ body_schema = body["content"]["application/json"]["schema"]
+ return body_schema.get("$ref", "").split("/")[-1]
diff --git a/tests/test_request_params/test_cookie/__init__.py b/tests/test_request_params/test_cookie/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_request_params/test_cookie/test_list.py b/tests/test_request_params/test_cookie/test_list.py
new file mode 100644
index 000000000..4ae80e001
--- /dev/null
+++ b/tests/test_request_params/test_cookie/test_list.py
@@ -0,0 +1,3 @@
+# Currently, there is no way to pass multiple cookies with the same name.
+# The only way to pass multiple values for cookie params is to serialize them using
+# a comma as a delimiter, but this is not currently supported by Starlette.
diff --git a/tests/test_request_params/test_cookie/test_optional_list.py b/tests/test_request_params/test_cookie/test_optional_list.py
new file mode 100644
index 000000000..4ae80e001
--- /dev/null
+++ b/tests/test_request_params/test_cookie/test_optional_list.py
@@ -0,0 +1,3 @@
+# Currently, there is no way to pass multiple cookies with the same name.
+# The only way to pass multiple values for cookie params is to serialize them using
+# a comma as a delimiter, but this is not currently supported by Starlette.
diff --git a/tests/test_request_params/test_cookie/test_optional_str.py b/tests/test_request_params/test_cookie/test_optional_str.py
new file mode 100644
index 000000000..1eec45689
--- /dev/null
+++ b/tests/test_request_params/test_cookie/test_optional_str.py
@@ -0,0 +1,362 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import Cookie, FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/optional-str")
+async def read_optional_str(p: Annotated[Optional[str], Cookie()] = None):
+ return {"p": p}
+
+
+class CookieModelOptionalStr(BaseModel):
+ p: Optional[str] = None
+
+
+@app.get("/model-optional-str")
+async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P",
+ },
+ "name": "p",
+ "in": "cookie",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {"title": "P", "type": "string"},
+ "name": "p",
+ "in": "cookie",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str(path: str):
+ client = TestClient(app)
+ client.cookies.set("p", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/optional-alias")
+async def read_optional_alias(
+ p: Annotated[Optional[str], Cookie(alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class CookieModelOptionalAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias")
+
+
+@app.get("/model-optional-alias")
+async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cookie()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Alias",
+ },
+ "name": "p_alias",
+ "in": "cookie",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {"title": "P Alias", "type": "string"},
+ "name": "p_alias",
+ "in": "cookie",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_by_name(path: str):
+ client = TestClient(app)
+ client.cookies.set("p", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias",
+ "/model-optional-alias",
+ ],
+)
+def test_optional_alias_by_alias(path: str):
+ client = TestClient(app)
+ client.cookies.set("p_alias", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/optional-validation-alias")
+def read_optional_validation_alias(
+ p: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None,
+):
+ return {"p": p}
+
+
+class CookieModelOptionalValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, validation_alias="p_val_alias")
+
+
+@app.get("/model-optional-validation-alias")
+def read_model_optional_validation_alias(
+ p: Annotated[CookieModelOptionalValidationAlias, Cookie()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "cookie",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ client.cookies.set("p", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ client.cookies.set("p_val_alias", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/optional-alias-and-validation-alias")
+def read_optional_alias_and_validation_alias(
+ p: Annotated[
+ Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class CookieModelOptionalAliasAndValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.get("/model-optional-alias-and-validation-alias")
+def read_model_optional_alias_and_validation_alias(
+ p: Annotated[CookieModelOptionalAliasAndValidationAlias, Cookie()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "cookie",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ client.cookies.set("p", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ client.cookies.set("p_alias", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ client.cookies.set("p_val_alias", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_cookie/test_required_str.py b/tests/test_request_params/test_cookie/test_required_str.py
new file mode 100644
index 000000000..6d0fa2ef2
--- /dev/null
+++ b/tests/test_request_params/test_cookie/test_required_str.py
@@ -0,0 +1,462 @@
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import Cookie, FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/required-str")
+async def read_required_str(p: Annotated[str, Cookie()]):
+ return {"p": p}
+
+
+class CookieModelRequiredStr(BaseModel):
+ p: str
+
+
+@app.get("/model-required-str")
+async def read_model_required_str(p: Annotated[CookieModelRequiredStr, Cookie()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P", "type": "string"},
+ "name": "p",
+ "in": "cookie",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["cookie", "p"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["cookie", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str(path: str):
+ client = TestClient(app)
+ client.cookies.set("p", "hello")
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/required-alias")
+async def read_required_alias(p: Annotated[str, Cookie(alias="p_alias")]):
+ return {"p": p}
+
+
+class CookieModelRequiredAlias(BaseModel):
+ p: str = Field(alias="p_alias")
+
+
+@app.get("/model-required-alias")
+async def read_model_required_alias(p: Annotated[CookieModelRequiredAlias, Cookie()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Alias", "type": "string"},
+ "name": "p_alias",
+ "in": "cookie",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["cookie", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["cookie", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias",
+ "/model-required-alias",
+ ],
+)
+def test_required_alias_by_name(path: str):
+ client = TestClient(app)
+ client.cookies.set("p", "hello")
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["cookie", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {"p": "hello"},
+ ),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["cookie", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias",
+ "/model-required-alias",
+ ],
+)
+def test_required_alias_by_alias(path: str):
+ client = TestClient(app)
+ client.cookies.set("p_alias", "hello")
+ response = client.get(path)
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/required-validation-alias")
+def read_required_validation_alias(
+ p: Annotated[str, Cookie(validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class CookieModelRequiredValidationAlias(BaseModel):
+ p: str = Field(validation_alias="p_val_alias")
+
+
+@app.get("/model-required-validation-alias")
+def read_model_required_validation_alias(
+ p: Annotated[CookieModelRequiredValidationAlias, Cookie()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-validation-alias", "/model-required-validation-alias"],
+)
+def test_required_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Val Alias", "type": "string"},
+ "name": "p_val_alias",
+ "in": "cookie",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "cookie",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ client.cookies.set("p", "hello")
+ response = client.get(path)
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["cookie", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ client.cookies.set("p_val_alias", "hello")
+ response = client.get(path)
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/required-alias-and-validation-alias")
+def read_required_alias_and_validation_alias(
+ p: Annotated[str, Cookie(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class CookieModelRequiredAliasAndValidationAlias(BaseModel):
+ p: str = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.get("/model-required-alias-and-validation-alias")
+def read_model_required_alias_and_validation_alias(
+ p: Annotated[CookieModelRequiredAliasAndValidationAlias, Cookie()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Val Alias", "type": "string"},
+ "name": "p_val_alias",
+ "in": "cookie",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "cookie",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ client.cookies.set("p", "hello")
+ response = client.get(path)
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "cookie",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {"p": "hello"},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ client.cookies.set("p_alias", "hello")
+ response = client.get(path)
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["cookie", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {"p_alias": "hello"},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ client.cookies.set("p_val_alias", "hello")
+ response = client.get(path)
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_file/__init__.py b/tests/test_request_params/test_file/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py
new file mode 100644
index 000000000..94a33967f
--- /dev/null
+++ b/tests/test_request_params/test_file/test_list.py
@@ -0,0 +1,557 @@
+from typing import List
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, File, UploadFile
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/list-bytes", operation_id="list_bytes")
+async def read_list_bytes(p: Annotated[List[bytes], File()]):
+ return {"file_size": [len(file) for file in p]}
+
+
+@app.post("/list-uploadfile", operation_id="list_uploadfile")
+async def read_list_uploadfile(p: Annotated[List[UploadFile], File()]):
+ return {"file_size": [file.size for file in p]}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes",
+ "/list-uploadfile",
+ ],
+)
+def test_list_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p": (
+ IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ {"type": "null"},
+ ],
+ "title": "P",
+ },
+ )
+ | IsDict(
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ "title": "P",
+ },
+ )
+ )
+ },
+ "required": ["p"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes",
+ "/list-uploadfile",
+ ],
+)
+def test_list_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes",
+ "/list-uploadfile",
+ ],
+)
+def test_list(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
+ assert response.status_code == 200
+ assert response.json() == {"file_size": [5, 5]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/list-bytes-alias", operation_id="list_bytes_alias")
+async def read_list_bytes_alias(p: Annotated[List[bytes], File(alias="p_alias")]):
+ return {"file_size": [len(file) for file in p]}
+
+
+@app.post("/list-uploadfile-alias", operation_id="list_uploadfile_alias")
+async def read_list_uploadfile_alias(
+ p: Annotated[List[UploadFile], File(alias="p_alias")],
+):
+ return {"file_size": [file.size for file in p]}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias",
+ "/list-uploadfile-alias",
+ ],
+)
+def test_list_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ {"type": "null"},
+ ],
+ "title": "P Alias",
+ },
+ )
+ | IsDict(
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ "title": "P Alias",
+ },
+ )
+ )
+ },
+ "required": ["p_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias",
+ "/list-uploadfile-alias",
+ ],
+)
+def test_list_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias",
+ "/list-uploadfile-alias",
+ ],
+)
+def test_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias",
+ "/list-uploadfile-alias",
+ ],
+)
+def test_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": [5, 5]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post("/list-bytes-validation-alias", operation_id="list_bytes_validation_alias")
+def read_list_bytes_validation_alias(
+ p: Annotated[List[bytes], File(validation_alias="p_val_alias")],
+):
+ return {"file_size": [len(file) for file in p]}
+
+
+@app.post(
+ "/list-uploadfile-validation-alias",
+ operation_id="list_uploadfile_validation_alias",
+)
+def read_list_uploadfile_validation_alias(
+ p: Annotated[List[UploadFile], File(validation_alias="p_val_alias")],
+):
+ return {"file_size": [file.size for file in p]}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-validation-alias",
+ "/list-uploadfile-validation-alias",
+ ],
+)
+def test_list_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ )
+ | IsDict(
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ "title": "P Val Alias",
+ },
+ )
+ )
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-validation-alias",
+ "/list-uploadfile-validation-alias",
+ ],
+)
+def test_list_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-validation-alias",
+ "/list-uploadfile-validation-alias",
+ ],
+)
+def test_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-validation-alias",
+ "/list-uploadfile-validation-alias",
+ ],
+)
+def test_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(
+ path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": [5, 5]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/list-bytes-alias-and-validation-alias",
+ operation_id="list_bytes_alias_and_validation_alias",
+)
+def read_list_bytes_alias_and_validation_alias(
+ p: Annotated[List[bytes], File(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"file_size": [len(file) for file in p]}
+
+
+@app.post(
+ "/list-uploadfile-alias-and-validation-alias",
+ operation_id="list_uploadfile_alias_and_validation_alias",
+)
+def read_list_uploadfile_alias_and_validation_alias(
+ p: Annotated[
+ List[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
+ ],
+):
+ return {"file_size": [file.size for file in p]}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias-and-validation-alias",
+ "/list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_list_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ )
+ | IsDict(
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ "title": "P Val Alias",
+ },
+ )
+ )
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias-and-validation-alias",
+ "/list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_list_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias-and-validation-alias",
+ "/list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias-and-validation-alias",
+ "/list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/list-bytes-alias-and-validation-alias",
+ "/list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(
+ path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": [5, 5]}
diff --git a/tests/test_request_params/test_file/test_optional.py b/tests/test_request_params/test_file/test_optional.py
new file mode 100644
index 000000000..2c1ca9530
--- /dev/null
+++ b/tests/test_request_params/test_file/test_optional.py
@@ -0,0 +1,408 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, File, UploadFile
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/optional-bytes", operation_id="optional_bytes")
+async def read_optional_bytes(p: Annotated[Optional[bytes], File()] = None):
+ return {"file_size": len(p) if p else None}
+
+
+@app.post("/optional-uploadfile", operation_id="optional_uploadfile")
+async def read_optional_uploadfile(p: Annotated[Optional[UploadFile], File()] = None):
+ return {"file_size": p.size if p else None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes",
+ "/optional-uploadfile",
+ ],
+)
+def test_optional_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p": (
+ IsDict(
+ {
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ "title": "P",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "P", "type": "string", "format": "binary"}
+ )
+ ),
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes",
+ "/optional-uploadfile",
+ ],
+)
+def test_optional_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes",
+ "/optional-uploadfile",
+ ],
+)
+def test_optional(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello")])
+ assert response.status_code == 200
+ assert response.json() == {"file_size": 5}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/optional-bytes-alias", operation_id="optional_bytes_alias")
+async def read_optional_bytes_alias(
+ p: Annotated[Optional[bytes], File(alias="p_alias")] = None,
+):
+ return {"file_size": len(p) if p else None}
+
+
+@app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias")
+async def read_optional_uploadfile_alias(
+ p: Annotated[Optional[UploadFile], File(alias="p_alias")] = None,
+):
+ return {"file_size": p.size if p else None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias",
+ "/optional-uploadfile-alias",
+ ],
+)
+def test_optional_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ "title": "P Alias",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "P Alias", "type": "string", "format": "binary"}
+ )
+ ),
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias",
+ "/optional-uploadfile-alias",
+ ],
+)
+def test_optional_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias",
+ "/optional-uploadfile-alias",
+ ],
+)
+def test_optional_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello")])
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias",
+ "/optional-uploadfile-alias",
+ ],
+)
+def test_optional_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_alias", b"hello")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": 5}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post(
+ "/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias"
+)
+def read_optional_bytes_validation_alias(
+ p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None,
+):
+ return {"file_size": len(p) if p else None}
+
+
+@app.post(
+ "/optional-uploadfile-validation-alias",
+ operation_id="optional_uploadfile_validation_alias",
+)
+def read_optional_uploadfile_validation_alias(
+ p: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None,
+):
+ return {"file_size": p.size if p else None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-validation-alias",
+ "/optional-uploadfile-validation-alias",
+ ],
+)
+def test_optional_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "P Val Alias", "type": "string", "format": "binary"}
+ )
+ ),
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-validation-alias",
+ "/optional-uploadfile-validation-alias",
+ ],
+)
+def test_optional_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-validation-alias",
+ "/optional-uploadfile-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-validation-alias",
+ "/optional-uploadfile-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_val_alias", b"hello")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": 5}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/optional-bytes-alias-and-validation-alias",
+ operation_id="optional_bytes_alias_and_validation_alias",
+)
+def read_optional_bytes_alias_and_validation_alias(
+ p: Annotated[
+ Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"file_size": len(p) if p else None}
+
+
+@app.post(
+ "/optional-uploadfile-alias-and-validation-alias",
+ operation_id="optional_uploadfile_alias_and_validation_alias",
+)
+def read_optional_uploadfile_alias_and_validation_alias(
+ p: Annotated[
+ Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"file_size": p.size if p else None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias-and-validation-alias",
+ "/optional-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {"type": "string", "format": "binary"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {"title": "P Val Alias", "type": "string", "format": "binary"}
+ )
+ ),
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias-and-validation-alias",
+ "/optional-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias-and-validation-alias",
+ "/optional-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias-and-validation-alias",
+ "/optional-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_alias", b"hello")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-bytes-alias-and-validation-alias",
+ "/optional-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_val_alias", b"hello")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": 5}
diff --git a/tests/test_request_params/test_file/test_optional_list.py b/tests/test_request_params/test_file/test_optional_list.py
new file mode 100644
index 000000000..20e36501f
--- /dev/null
+++ b/tests/test_request_params/test_file/test_optional_list.py
@@ -0,0 +1,434 @@
+from typing import List, Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, File, UploadFile
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/optional-list-bytes")
+async def read_optional_list_bytes(p: Annotated[Optional[List[bytes]], File()] = None):
+ return {"file_size": [len(file) for file in p] if p else None}
+
+
+@app.post("/optional-list-uploadfile")
+async def read_optional_list_uploadfile(
+ p: Annotated[Optional[List[UploadFile]], File()] = None,
+):
+ return {"file_size": [file.size for file in p] if p else None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes",
+ "/optional-list-uploadfile",
+ ],
+)
+def test_optional_list_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p": (
+ IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ {"type": "null"},
+ ],
+ "title": "P",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "P",
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ )
+ ),
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes",
+ "/optional-list-uploadfile",
+ ],
+)
+def test_optional_list_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes",
+ "/optional-list-uploadfile",
+ ],
+)
+def test_optional_list(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
+ assert response.status_code == 200
+ assert response.json() == {"file_size": [5, 5]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/optional-list-bytes-alias")
+async def read_optional_list_bytes_alias(
+ p: Annotated[Optional[List[bytes]], File(alias="p_alias")] = None,
+):
+ return {"file_size": [len(file) for file in p] if p else None}
+
+
+@app.post("/optional-list-uploadfile-alias")
+async def read_optional_list_uploadfile_alias(
+ p: Annotated[Optional[List[UploadFile]], File(alias="p_alias")] = None,
+):
+ return {"file_size": [file.size for file in p] if p else None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias",
+ "/optional-list-uploadfile-alias",
+ ],
+)
+def test_optional_list_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ {"type": "null"},
+ ],
+ "title": "P Alias",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "P Alias",
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ }
+ )
+ ),
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias",
+ "/optional-list-uploadfile-alias",
+ ],
+)
+def test_optional_list_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias",
+ "/optional-list-uploadfile-alias",
+ ],
+)
+def test_optional_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias",
+ "/optional-list-uploadfile-alias",
+ ],
+)
+def test_optional_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": [5, 5]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post("/optional-list-bytes-validation-alias")
+def read_optional_list_bytes_validation_alias(
+ p: Annotated[Optional[List[bytes]], File(validation_alias="p_val_alias")] = None,
+):
+ return {"file_size": [len(file) for file in p] if p else None}
+
+
+@app.post("/optional-list-uploadfile-validation-alias")
+def read_optional_list_uploadfile_validation_alias(
+ p: Annotated[
+ Optional[List[UploadFile]], File(validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"file_size": [file.size for file in p] if p else None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-validation-alias",
+ "/optional-list-uploadfile-validation-alias",
+ ],
+)
+def test_optional_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "P Val Alias",
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ }
+ )
+ ),
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-validation-alias",
+ "/optional-list-uploadfile-validation-alias",
+ ],
+)
+def test_optional_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-validation-alias",
+ "/optional-list-uploadfile-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-validation-alias",
+ "/optional-list-uploadfile-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(
+ path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": [5, 5]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post("/optional-list-bytes-alias-and-validation-alias")
+def read_optional_list_bytes_alias_and_validation_alias(
+ p: Annotated[
+ Optional[List[bytes]], File(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"file_size": [len(file) for file in p] if p else None}
+
+
+@app.post("/optional-list-uploadfile-alias-and-validation-alias")
+def read_optional_list_uploadfile_alias_and_validation_alias(
+ p: Annotated[
+ Optional[List[UploadFile]],
+ File(alias="p_alias", validation_alias="p_val_alias"),
+ ] = None,
+):
+ return {"file_size": [file.size for file in p] if p else None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias-and-validation-alias",
+ "/optional-list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": (
+ IsDict(
+ {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ },
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "title": "P Val Alias",
+ "type": "array",
+ "items": {"type": "string", "format": "binary"},
+ }
+ )
+ ),
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias-and-validation-alias",
+ "/optional-list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias-and-validation-alias",
+ "/optional-list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias-and-validation-alias",
+ "/optional-list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-bytes-alias-and-validation-alias",
+ "/optional-list-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(
+ path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": [5, 5]}
diff --git a/tests/test_request_params/test_file/test_required.py b/tests/test_request_params/test_file/test_required.py
new file mode 100644
index 000000000..f041ac1cc
--- /dev/null
+++ b/tests/test_request_params/test_file/test_required.py
@@ -0,0 +1,479 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, File, UploadFile
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/required-bytes", operation_id="required_bytes")
+async def read_required_bytes(p: Annotated[bytes, File()]):
+ return {"file_size": len(p)}
+
+
+@app.post("/required-uploadfile", operation_id="required_uploadfile")
+async def read_required_uploadfile(p: Annotated[UploadFile, File()]):
+ return {"file_size": p.size}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes",
+ "/required-uploadfile",
+ ],
+)
+def test_required_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p": {"title": "P", "type": "string", "format": "binary"},
+ },
+ "required": ["p"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes",
+ "/required-uploadfile",
+ ],
+)
+def test_required_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes",
+ "/required-uploadfile",
+ ],
+)
+def test_required(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello")])
+ assert response.status_code == 200
+ assert response.json() == {"file_size": 5}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/required-bytes-alias", operation_id="required_bytes_alias")
+async def read_required_bytes_alias(p: Annotated[bytes, File(alias="p_alias")]):
+ return {"file_size": len(p)}
+
+
+@app.post("/required-uploadfile-alias", operation_id="required_uploadfile_alias")
+async def read_required_uploadfile_alias(
+ p: Annotated[UploadFile, File(alias="p_alias")],
+):
+ return {"file_size": p.size}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias",
+ "/required-uploadfile-alias",
+ ],
+)
+def test_required_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_alias": {"title": "P Alias", "type": "string", "format": "binary"},
+ },
+ "required": ["p_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias",
+ "/required-uploadfile-alias",
+ ],
+)
+def test_required_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias",
+ "/required-uploadfile-alias",
+ ],
+)
+def test_required_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello")])
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias",
+ "/required-uploadfile-alias",
+ ],
+)
+def test_required_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_alias", b"hello")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": 5}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post(
+ "/required-bytes-validation-alias", operation_id="required_bytes_validation_alias"
+)
+def read_required_bytes_validation_alias(
+ p: Annotated[bytes, File(validation_alias="p_val_alias")],
+):
+ return {"file_size": len(p)}
+
+
+@app.post(
+ "/required-uploadfile-validation-alias",
+ operation_id="required_uploadfile_validation_alias",
+)
+def read_required_uploadfile_validation_alias(
+ p: Annotated[UploadFile, File(validation_alias="p_val_alias")],
+):
+ return {"file_size": p.size}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-validation-alias",
+ "/required-uploadfile-validation-alias",
+ ],
+)
+def test_required_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {
+ "title": "P Val Alias",
+ "type": "string",
+ "format": "binary",
+ },
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-validation-alias",
+ "/required-uploadfile-validation-alias",
+ ],
+)
+def test_required_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-validation-alias",
+ "/required-uploadfile-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p", b"hello")])
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-validation-alias",
+ "/required-uploadfile-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_val_alias", b"hello")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": 5}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/required-bytes-alias-and-validation-alias",
+ operation_id="required_bytes_alias_and_validation_alias",
+)
+def read_required_bytes_alias_and_validation_alias(
+ p: Annotated[bytes, File(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"file_size": len(p)}
+
+
+@app.post(
+ "/required-uploadfile-alias-and-validation-alias",
+ operation_id="required_uploadfile_alias_and_validation_alias",
+)
+def read_required_uploadfile_alias_and_validation_alias(
+ p: Annotated[UploadFile, File(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"file_size": p.size}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias-and-validation-alias",
+ "/required-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {
+ "title": "P Val Alias",
+ "type": "string",
+ "format": "binary",
+ },
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias-and-validation-alias",
+ "/required-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias-and-validation-alias",
+ "/required-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, files={"p": "hello"})
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias-and-validation-alias",
+ "/required-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_alias", b"hello")])
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-bytes-alias-and-validation-alias",
+ "/required-uploadfile-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, files=[("p_val_alias", b"hello")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"file_size": 5}
diff --git a/tests/test_request_params/test_file/utils.py b/tests/test_request_params/test_file/utils.py
new file mode 100644
index 000000000..e33f64385
--- /dev/null
+++ b/tests/test_request_params/test_file/utils.py
@@ -0,0 +1,7 @@
+from typing import Any, Dict
+
+
+def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
+ body = openapi["paths"][path]["post"]["requestBody"]
+ body_schema = body["content"]["multipart/form-data"]["schema"]
+ return body_schema.get("$ref", "").split("/")[-1]
diff --git a/tests/test_request_params/test_form/__init__.py b/tests/test_request_params/test_form/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_request_params/test_form/test_list.py b/tests/test_request_params/test_form/test_list.py
new file mode 100644
index 000000000..69a1b6a38
--- /dev/null
+++ b/tests/test_request_params/test_form/test_list.py
@@ -0,0 +1,486 @@
+from typing import List
+
+import pytest
+from dirty_equals import IsDict, IsOneOf, IsPartialDict
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/required-list-str", operation_id="required_list_str")
+async def read_required_list_str(p: Annotated[List[str], Form()]):
+ return {"p": p}
+
+
+class FormModelRequiredListStr(BaseModel):
+ p: List[str]
+
+
+@app.post("/model-required-list-str", operation_id="model_required_list_str")
+def read_model_required_list_str(p: Annotated[FormModelRequiredListStr, Form()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p": {
+ "items": {"type": "string"},
+ "title": "P",
+ "type": "array",
+ },
+ },
+ "required": ["p"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ {
+ "detail": [
+ {
+ "loc": ["body", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/required-list-alias", operation_id="required_list_alias")
+async def read_required_list_alias(p: Annotated[List[str], Form(alias="p_alias")]):
+ return {"p": p}
+
+
+class FormModelRequiredListAlias(BaseModel):
+ p: List[str] = Field(alias="p_alias")
+
+
+@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
+async def read_model_required_list_alias(
+ p: Annotated[FormModelRequiredListAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias",
+ "/model-required-list-alias",
+ ],
+)
+def test_required_list_str_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_alias": {
+ "items": {"type": "string"},
+ "title": "P Alias",
+ "type": "array",
+ },
+ },
+ "required": ["p_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias",
+ "/model-required-list-alias",
+ ],
+)
+def test_required_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": ["hello", "world"]})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": ["hello", "world"]}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post(
+ "/required-list-validation-alias", operation_id="required_list_validation_alias"
+)
+def read_required_list_validation_alias(
+ p: Annotated[List[str], Form(validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class FormModelRequiredListValidationAlias(BaseModel):
+ p: List[str] = Field(validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-required-list-validation-alias",
+ operation_id="model_required_list_validation_alias",
+)
+async def read_model_required_list_validation_alias(
+ p: Annotated[FormModelRequiredListValidationAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-validation-alias", "/model-required-list-validation-alias"],
+)
+def test_required_list_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {
+ "items": {"type": "string"},
+ "title": "P Val Alias",
+ "type": "array",
+ },
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": ["hello", "world"]})
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-validation-alias", "/model-required-list-validation-alias"],
+)
+def test_required_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_val_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/required-list-alias-and-validation-alias",
+ operation_id="required_list_alias_and_validation_alias",
+)
+def read_required_list_alias_and_validation_alias(
+ p: Annotated[List[str], Form(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class FormModelRequiredListAliasAndValidationAlias(BaseModel):
+ p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-required-list-alias-and-validation-alias",
+ operation_id="model_required_list_alias_and_validation_alias",
+)
+def read_model_required_list_alias_and_validation_alias(
+ p: Annotated[FormModelRequiredListAliasAndValidationAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {
+ "items": {"type": "string"},
+ "title": "P Val Alias",
+ "type": "array",
+ },
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": ["hello", "world"]})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {"p": ["hello", "world"]},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_alias": ["hello", "world"]})
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_val_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
diff --git a/tests/test_request_params/test_form/test_optional_list.py b/tests/test_request_params/test_form/test_optional_list.py
new file mode 100644
index 000000000..1f779a7ed
--- /dev/null
+++ b/tests/test_request_params/test_form/test_optional_list.py
@@ -0,0 +1,429 @@
+from typing import List, Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/optional-list-str", operation_id="optional_list_str")
+async def read_optional_list_str(
+ p: Annotated[Optional[List[str]], Form()] = None,
+):
+ return {"p": p}
+
+
+class FormModelOptionalListStr(BaseModel):
+ p: Optional[List[str]] = None
+
+
+@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
+async def read_model_optional_list_str(p: Annotated[FormModelOptionalListStr, Form()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p": {"items": {"type": "string"}, "type": "array", "title": "P"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/optional-list-alias", operation_id="optional_list_alias")
+async def read_optional_list_alias(
+ p: Annotated[Optional[List[str]], Form(alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class FormModelOptionalListAlias(BaseModel):
+ p: Optional[List[str]] = Field(None, alias="p_alias")
+
+
+@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
+async def read_model_optional_list_alias(
+ p: Annotated[FormModelOptionalListAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias",
+ "/model-optional-list-alias",
+ ],
+)
+def test_optional_list_str_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_alias": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_alias": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "P Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_alias": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post(
+ "/optional-list-validation-alias", operation_id="optional_list_validation_alias"
+)
+def read_optional_list_validation_alias(
+ p: Annotated[Optional[List[str]], Form(validation_alias="p_val_alias")] = None,
+):
+ return {"p": p}
+
+
+class FormModelOptionalListValidationAlias(BaseModel):
+ p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-optional-list-validation-alias",
+ operation_id="model_optional_list_validation_alias",
+)
+def read_model_optional_list_validation_alias(
+ p: Annotated[FormModelOptionalListValidationAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_val_alias": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_val_alias": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-validation-alias",
+ "/model-optional-list-validation-alias",
+ ],
+)
+def test_optional_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_val_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/optional-list-alias-and-validation-alias",
+ operation_id="optional_list_alias_and_validation_alias",
+)
+def read_optional_list_alias_and_validation_alias(
+ p: Annotated[
+ Optional[List[str]], Form(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class FormModelOptionalListAliasAndValidationAlias(BaseModel):
+ p: Optional[List[str]] = Field(
+ None, alias="p_alias", validation_alias="p_val_alias"
+ )
+
+
+@app.post(
+ "/model-optional-list-alias-and-validation-alias",
+ operation_id="model_optional_list_alias_and_validation_alias",
+)
+def read_model_optional_list_alias_and_validation_alias(
+ p: Annotated[FormModelOptionalListAliasAndValidationAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_val_alias": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_val_alias": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_alias": ["hello", "world"]})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_val_alias": ["hello", "world"]})
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "p": [
+ "hello",
+ "world",
+ ]
+ }
diff --git a/tests/test_request_params/test_form/test_optional_str.py b/tests/test_request_params/test_form/test_optional_str.py
new file mode 100644
index 000000000..969865945
--- /dev/null
+++ b/tests/test_request_params/test_form/test_optional_str.py
@@ -0,0 +1,394 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/optional-str", operation_id="optional_str")
+async def read_optional_str(p: Annotated[Optional[str], Form()] = None):
+ return {"p": p}
+
+
+class FormModelOptionalStr(BaseModel):
+ p: Optional[str] = None
+
+
+@app.post("/model-optional-str", operation_id="model_optional_str")
+async def read_model_optional_str(p: Annotated[FormModelOptionalStr, Form()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p": {"type": "string", "title": "P"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/optional-alias", operation_id="optional_alias")
+async def read_optional_alias(
+ p: Annotated[Optional[str], Form(alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class FormModelOptionalAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias")
+
+
+@app.post("/model-optional-alias", operation_id="model_optional_alias")
+async def read_model_optional_alias(p: Annotated[FormModelOptionalAlias, Form()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias",
+ "/model-optional-alias",
+ ],
+)
+def test_optional_str_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_alias": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_alias": {"type": "string", "title": "P Alias"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
+def read_optional_validation_alias(
+ p: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None,
+):
+ return {"p": p}
+
+
+class FormModelOptionalValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-optional-validation-alias", operation_id="model_optional_validation_alias"
+)
+def read_model_optional_validation_alias(
+ p: Annotated[FormModelOptionalValidationAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_val_alias": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_val_alias": {"type": "string", "title": "P Val Alias"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_val_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/optional-alias-and-validation-alias",
+ operation_id="optional_alias_and_validation_alias",
+)
+def read_optional_alias_and_validation_alias(
+ p: Annotated[
+ Optional[str], Form(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class FormModelOptionalAliasAndValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-optional-alias-and-validation-alias",
+ operation_id="model_optional_alias_and_validation_alias",
+)
+def read_model_optional_alias_and_validation_alias(
+ p: Annotated[FormModelOptionalAliasAndValidationAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
+ {
+ "properties": {
+ "p_val_alias": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "properties": {
+ "p_val_alias": {"type": "string", "title": "P Val Alias"},
+ },
+ "title": body_model_name,
+ "type": "object",
+ }
+ )
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_val_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_form/test_required_str.py b/tests/test_request_params/test_form/test_required_str.py
new file mode 100644
index 000000000..c901e7b44
--- /dev/null
+++ b/tests/test_request_params/test_form/test_required_str.py
@@ -0,0 +1,464 @@
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+from .utils import get_body_model_name
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.post("/required-str", operation_id="required_str")
+async def read_required_str(p: Annotated[str, Form()]):
+ return {"p": p}
+
+
+class FormModelRequiredStr(BaseModel):
+ p: str
+
+
+@app.post("/model-required-str", operation_id="model_required_str")
+async def read_model_required_str(p: Annotated[FormModelRequiredStr, Form()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p": {"title": "P", "type": "string"},
+ },
+ "required": ["p"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.post("/required-alias", operation_id="required_alias")
+async def read_required_alias(p: Annotated[str, Form(alias="p_alias")]):
+ return {"p": p}
+
+
+class FormModelRequiredAlias(BaseModel):
+ p: str = Field(alias="p_alias")
+
+
+@app.post("/model-required-alias", operation_id="model_required_alias")
+async def read_model_required_alias(p: Annotated[FormModelRequiredAlias, Form()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias",
+ "/model-required-alias",
+ ],
+)
+def test_required_str_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_alias": {"title": "P Alias", "type": "string"},
+ },
+ "required": ["p_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": "hello"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["body", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_alias": "hello"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.post("/required-validation-alias", operation_id="required_validation_alias")
+def read_required_validation_alias(
+ p: Annotated[str, Form(validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class FormModelRequiredValidationAlias(BaseModel):
+ p: str = Field(validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-required-validation-alias", operation_id="model_required_validation_alias"
+)
+def read_model_required_validation_alias(
+ p: Annotated[FormModelRequiredValidationAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-validation-alias", "/model-required-validation-alias"],
+)
+def test_required_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {"title": "P Val Alias", "type": "string"},
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": "hello"})
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_val_alias": "hello"})
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.post(
+ "/required-alias-and-validation-alias",
+ operation_id="required_alias_and_validation_alias",
+)
+def read_required_alias_and_validation_alias(
+ p: Annotated[str, Form(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class FormModelRequiredAliasAndValidationAlias(BaseModel):
+ p: str = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.post(
+ "/model-required-alias-and-validation-alias",
+ operation_id="model_required_alias_and_validation_alias",
+)
+def read_model_required_alias_and_validation_alias(
+ p: Annotated[FormModelRequiredAliasAndValidationAlias, Form()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_schema(path: str):
+ openapi = app.openapi()
+ body_model_name = get_body_model_name(openapi, path)
+
+ assert app.openapi()["components"]["schemas"][body_model_name] == {
+ "properties": {
+ "p_val_alias": {"title": "P Val Alias", "type": "string"},
+ },
+ "required": ["p_val_alias"],
+ "title": body_model_name,
+ "type": "object",
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.post(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p": "hello"})
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_alias": "hello"})
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p_alias": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.post(path, data={"p_val_alias": "hello"})
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_form/utils.py b/tests/test_request_params/test_form/utils.py
new file mode 100644
index 000000000..d200650df
--- /dev/null
+++ b/tests/test_request_params/test_form/utils.py
@@ -0,0 +1,7 @@
+from typing import Any, Dict
+
+
+def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
+ body = openapi["paths"][path]["post"]["requestBody"]
+ body_schema = body["content"]["application/x-www-form-urlencoded"]["schema"]
+ return body_schema.get("$ref", "").split("/")[-1]
diff --git a/tests/test_request_params/test_header/__init__.py b/tests/test_request_params/test_header/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_request_params/test_header/test_list.py b/tests/test_request_params/test_header/test_list.py
new file mode 100644
index 000000000..4a5f4bb64
--- /dev/null
+++ b/tests/test_request_params/test_header/test_list.py
@@ -0,0 +1,468 @@
+from typing import List
+
+import pytest
+from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
+from fastapi import FastAPI, Header
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/required-list-str")
+async def read_required_list_str(p: Annotated[List[str], Header()]):
+ return {"p": p}
+
+
+class HeaderModelRequiredListStr(BaseModel):
+ p: List[str]
+
+
+@app.get("/model-required-list-str")
+def read_model_required_list_str(p: Annotated[HeaderModelRequiredListStr, Header()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {
+ "title": "P",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": "p",
+ "in": "header",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p"],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+ ) | IsDict(
+ {
+ "detail": [
+ {
+ "loc": ["header", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/required-list-alias")
+async def read_required_list_alias(p: Annotated[List[str], Header(alias="p_alias")]):
+ return {"p": p}
+
+
+class HeaderModelRequiredListAlias(BaseModel):
+ p: List[str] = Field(alias="p_alias")
+
+
+@app.get("/model-required-list-alias")
+async def read_model_required_list_alias(
+ p: Annotated[HeaderModelRequiredListAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {
+ "title": "P Alias",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": "p_alias",
+ "in": "header",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_alias"],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias",
+ "/model-required-list-alias",
+ ],
+)
+def test_required_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias",
+ "/model-required-list-alias",
+ ],
+)
+def test_required_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/required-list-validation-alias")
+def read_required_list_validation_alias(
+ p: Annotated[List[str], Header(validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class HeaderModelRequiredListValidationAlias(BaseModel):
+ p: List[str] = Field(validation_alias="p_val_alias")
+
+
+@app.get("/model-required-list-validation-alias")
+async def read_model_required_list_validation_alias(
+ p: Annotated[HeaderModelRequiredListValidationAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-validation-alias", "/model-required-list-validation-alias"],
+)
+def test_required_list_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {
+ "title": "P Val Alias",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": "p_val_alias",
+ "in": "header",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "header",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-validation-alias", "/model-required-list-validation-alias"],
+)
+def test_required_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(
+ path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
+ )
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/required-list-alias-and-validation-alias")
+def read_required_list_alias_and_validation_alias(
+ p: Annotated[List[str], Header(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class HeaderModelRequiredListAliasAndValidationAlias(BaseModel):
+ p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.get("/model-required-list-alias-and-validation-alias")
+def read_model_required_list_alias_and_validation_alias(
+ p: Annotated[HeaderModelRequiredListAliasAndValidationAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {
+ "title": "P Val Alias",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": "p_val_alias",
+ "in": "header",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "header",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "header",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ IsPartialDict({"p": ["hello", "world"]}),
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ IsPartialDict({"p_alias": ["hello", "world"]}),
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(
+ path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
diff --git a/tests/test_request_params/test_header/test_optional_list.py b/tests/test_request_params/test_header/test_optional_list.py
new file mode 100644
index 000000000..e81025c02
--- /dev/null
+++ b/tests/test_request_params/test_header/test_optional_list.py
@@ -0,0 +1,384 @@
+from typing import List, Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Header
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/optional-list-str")
+async def read_optional_list_str(
+ p: Annotated[Optional[List[str]], Header()] = None,
+):
+ return {"p": p}
+
+
+class HeaderModelOptionalListStr(BaseModel):
+ p: Optional[List[str]] = None
+
+
+@app.get("/model-optional-list-str")
+async def read_model_optional_list_str(
+ p: Annotated[HeaderModelOptionalListStr, Header()],
+):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P",
+ },
+ "name": "p",
+ "in": "header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
+ "name": "p",
+ "in": "header",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/optional-list-alias")
+async def read_optional_list_alias(
+ p: Annotated[Optional[List[str]], Header(alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class HeaderModelOptionalListAlias(BaseModel):
+ p: Optional[List[str]] = Field(None, alias="p_alias")
+
+
+@app.get("/model-optional-list-alias")
+async def read_model_optional_list_alias(
+ p: Annotated[HeaderModelOptionalListAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Alias",
+ },
+ "name": "p_alias",
+ "in": "header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "P Alias",
+ },
+ "name": "p_alias",
+ "in": "header",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias",
+ "/model-optional-list-alias",
+ ],
+)
+def test_optional_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/optional-list-validation-alias")
+def read_optional_list_validation_alias(
+ p: Annotated[Optional[List[str]], Header(validation_alias="p_val_alias")] = None,
+):
+ return {"p": p}
+
+
+class HeaderModelOptionalListValidationAlias(BaseModel):
+ p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
+
+
+@app.get("/model-optional-list-validation-alias")
+def read_model_optional_list_validation_alias(
+ p: Annotated[HeaderModelOptionalListValidationAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "header",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-validation-alias",
+ "/model-optional-list-validation-alias",
+ ],
+)
+def test_optional_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(
+ path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/optional-list-alias-and-validation-alias")
+def read_optional_list_alias_and_validation_alias(
+ p: Annotated[
+ Optional[List[str]], Header(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class HeaderModelOptionalListAliasAndValidationAlias(BaseModel):
+ p: Optional[List[str]] = Field(
+ None, alias="p_alias", validation_alias="p_val_alias"
+ )
+
+
+@app.get("/model-optional-list-alias-and-validation-alias")
+def read_model_optional_list_alias_and_validation_alias(
+ p: Annotated[HeaderModelOptionalListAliasAndValidationAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "header",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p", "hello"), ("p", "world")])
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(
+ path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "p": [
+ "hello",
+ "world",
+ ]
+ }
diff --git a/tests/test_request_params/test_header/test_optional_str.py b/tests/test_request_params/test_header/test_optional_str.py
new file mode 100644
index 000000000..5ae9f2670
--- /dev/null
+++ b/tests/test_request_params/test_header/test_optional_str.py
@@ -0,0 +1,354 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Header
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/optional-str")
+async def read_optional_str(p: Annotated[Optional[str], Header()] = None):
+ return {"p": p}
+
+
+class HeaderModelOptionalStr(BaseModel):
+ p: Optional[str] = None
+
+
+@app.get("/model-optional-str")
+async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P",
+ },
+ "name": "p",
+ "in": "header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {"title": "P", "type": "string"},
+ "name": "p",
+ "in": "header",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/optional-alias")
+async def read_optional_alias(
+ p: Annotated[Optional[str], Header(alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class HeaderModelOptionalAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias")
+
+
+@app.get("/model-optional-alias")
+async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Header()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Alias",
+ },
+ "name": "p_alias",
+ "in": "header",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {"title": "P Alias", "type": "string"},
+ "name": "p_alias",
+ "in": "header",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias",
+ "/model-optional-alias",
+ ],
+)
+def test_optional_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/optional-validation-alias")
+def read_optional_validation_alias(
+ p: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None,
+):
+ return {"p": p}
+
+
+class HeaderModelOptionalValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, validation_alias="p_val_alias")
+
+
+@app.get("/model-optional-validation-alias")
+def read_model_optional_validation_alias(
+ p: Annotated[HeaderModelOptionalValidationAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "header",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_val_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/optional-alias-and-validation-alias")
+def read_optional_alias_and_validation_alias(
+ p: Annotated[
+ Optional[str], Header(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class HeaderModelOptionalAliasAndValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.get("/model-optional-alias-and-validation-alias")
+def read_model_optional_alias_and_validation_alias(
+ p: Annotated[HeaderModelOptionalAliasAndValidationAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "header",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_val_alias": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_header/test_required_str.py b/tests/test_request_params/test_header/test_required_str.py
new file mode 100644
index 000000000..d57c66919
--- /dev/null
+++ b/tests/test_request_params/test_header/test_required_str.py
@@ -0,0 +1,451 @@
+import pytest
+from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
+from fastapi import FastAPI, Header
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/required-str")
+async def read_required_str(p: Annotated[str, Header()]):
+ return {"p": p}
+
+
+class HeaderModelRequiredStr(BaseModel):
+ p: str
+
+
+@app.get("/model-required-str")
+async def read_model_required_str(p: Annotated[HeaderModelRequiredStr, Header()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P", "type": "string"},
+ "name": "p",
+ "in": "header",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p"],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p": "hello"})
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/required-alias")
+async def read_required_alias(p: Annotated[str, Header(alias="p_alias")]):
+ return {"p": p}
+
+
+class HeaderModelRequiredAlias(BaseModel):
+ p: str = Field(alias="p_alias")
+
+
+@app.get("/model-required-alias")
+async def read_model_required_alias(p: Annotated[HeaderModelRequiredAlias, Header()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Alias", "type": "string"},
+ "name": "p_alias",
+ "in": "header",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_alias"],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias",
+ "/model-required-alias",
+ ],
+)
+def test_required_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p": "hello"})
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, IsPartialDict({"p": "hello"})),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["header", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias",
+ "/model-required-alias",
+ ],
+)
+def test_required_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_alias": "hello"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/required-validation-alias")
+def read_required_validation_alias(
+ p: Annotated[str, Header(validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class HeaderModelRequiredValidationAlias(BaseModel):
+ p: str = Field(validation_alias="p_val_alias")
+
+
+@app.get("/model-required-validation-alias")
+def read_model_required_validation_alias(
+ p: Annotated[HeaderModelRequiredValidationAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-validation-alias", "/model-required-validation-alias"],
+)
+def test_required_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Val Alias", "type": "string"},
+ "name": "p_val_alias",
+ "in": "header",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "header",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p": "hello"})
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, IsPartialDict({"p": "hello"})),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_val_alias": "hello"})
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/required-alias-and-validation-alias")
+def read_required_alias_and_validation_alias(
+ p: Annotated[str, Header(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class HeaderModelRequiredAliasAndValidationAlias(BaseModel):
+ p: str = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.get("/model-required-alias-and-validation-alias")
+def read_model_required_alias_and_validation_alias(
+ p: Annotated[HeaderModelRequiredAliasAndValidationAlias, Header()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Val Alias", "type": "string"},
+ "name": "p_val_alias",
+ "in": "header",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "header",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p": "hello"})
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "header",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ IsPartialDict({"p": "hello"}),
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_alias": "hello"})
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ IsPartialDict({"p_alias": "hello"}),
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_val_alias": "hello"})
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_path/__init__.py b/tests/test_request_params/test_path/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_request_params/test_path/test_list.py b/tests/test_request_params/test_path/test_list.py
new file mode 100644
index 000000000..bba055d9a
--- /dev/null
+++ b/tests/test_request_params/test_path/test_list.py
@@ -0,0 +1 @@
+# FastAPI doesn't currently support non-scalar Path parameters
diff --git a/tests/test_request_params/test_path/test_optional_list.py b/tests/test_request_params/test_path/test_optional_list.py
new file mode 100644
index 000000000..0719430ac
--- /dev/null
+++ b/tests/test_request_params/test_path/test_optional_list.py
@@ -0,0 +1 @@
+# Optional Path parameters are not supported
diff --git a/tests/test_request_params/test_path/test_optional_str.py b/tests/test_request_params/test_path/test_optional_str.py
new file mode 100644
index 000000000..0719430ac
--- /dev/null
+++ b/tests/test_request_params/test_path/test_optional_str.py
@@ -0,0 +1 @@
+# Optional Path parameters are not supported
diff --git a/tests/test_request_params/test_path/test_required_str.py b/tests/test_request_params/test_path/test_required_str.py
new file mode 100644
index 000000000..641719967
--- /dev/null
+++ b/tests/test_request_params/test_path/test_required_str.py
@@ -0,0 +1,90 @@
+import pytest
+from fastapi import FastAPI, Path
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+
+@app.get("/required-str/{p}")
+async def read_required_str(p: Annotated[str, Path()]):
+ return {"p": p}
+
+
+@app.get("/required-alias/{p_alias}")
+async def read_required_alias(p: Annotated[str, Path(alias="p_alias")]):
+ return {"p": p}
+
+
+@app.get("/required-validation-alias/{p_val_alias}")
+def read_required_validation_alias(
+ p: Annotated[str, Path(validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+@app.get("/required-alias-and-validation-alias/{p_val_alias}")
+def read_required_alias_and_validation_alias(
+ p: Annotated[str, Path(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+@pytest.mark.parametrize(
+ ("path", "expected_name", "expected_title"),
+ [
+ pytest.param("/required-str/{p}", "p", "P", id="required-str"),
+ pytest.param(
+ "/required-alias/{p_alias}", "p_alias", "P Alias", id="required-alias"
+ ),
+ pytest.param(
+ "/required-validation-alias/{p_val_alias}",
+ "p_val_alias",
+ "P Val Alias",
+ id="required-validation-alias",
+ marks=needs_pydanticv2,
+ ),
+ pytest.param(
+ "/required-alias-and-validation-alias/{p_val_alias}",
+ "p_val_alias",
+ "P Val Alias",
+ id="required-alias-and-validation-alias",
+ marks=needs_pydanticv2,
+ ),
+ ],
+)
+def test_schema(path: str, expected_name: str, expected_title: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": expected_title, "type": "string"},
+ "name": expected_name,
+ "in": "path",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param("/required-str", id="required-str"),
+ pytest.param("/required-alias", id="required-alias"),
+ pytest.param(
+ "/required-validation-alias",
+ id="required-validation-alias",
+ marks=needs_pydanticv2,
+ ),
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ id="required-alias-and-validation-alias",
+ marks=needs_pydanticv2,
+ ),
+ ],
+)
+def test_success(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}/hello")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_query/__init__.py b/tests/test_request_params/test_query/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_request_params/test_query/test_list.py b/tests/test_request_params/test_query/test_list.py
new file mode 100644
index 000000000..71cbca51f
--- /dev/null
+++ b/tests/test_request_params/test_query/test_list.py
@@ -0,0 +1,469 @@
+from typing import List
+
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import FastAPI, Query
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/required-list-str")
+async def read_required_list_str(p: Annotated[List[str], Query()]):
+ return {"p": p}
+
+
+class QueryModelRequiredListStr(BaseModel):
+ p: List[str]
+
+
+@app.get("/model-required-list-str")
+def read_model_required_list_str(p: Annotated[QueryModelRequiredListStr, Query()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {
+ "title": "P",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": "p",
+ "in": "query",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ {
+ "detail": [
+ {
+ "loc": ["query", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-str", "/model-required-list-str"],
+)
+def test_required_list_str(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello&p=world")
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/required-list-alias")
+async def read_required_list_alias(p: Annotated[List[str], Query(alias="p_alias")]):
+ return {"p": p}
+
+
+class QueryModelRequiredListAlias(BaseModel):
+ p: List[str] = Field(alias="p_alias")
+
+
+@app.get("/model-required-list-alias")
+async def read_model_required_list_alias(
+ p: Annotated[QueryModelRequiredListAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {
+ "title": "P Alias",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": "p_alias",
+ "in": "query",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-alias", "/model-required-list-alias"],
+)
+def test_required_list_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias",
+ "/model-required-list-alias",
+ ],
+)
+def test_required_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello&p=world")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": ["hello", "world"]}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias",
+ "/model-required-list-alias",
+ ],
+)
+def test_required_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello&p_alias=world")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/required-list-validation-alias")
+def read_required_list_validation_alias(
+ p: Annotated[List[str], Query(validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class QueryModelRequiredListValidationAlias(BaseModel):
+ p: List[str] = Field(validation_alias="p_val_alias")
+
+
+@app.get("/model-required-list-validation-alias")
+async def read_model_required_list_validation_alias(
+ p: Annotated[QueryModelRequiredListValidationAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-validation-alias", "/model-required-list-validation-alias"],
+)
+def test_required_list_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {
+ "title": "P Val Alias",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": "p_val_alias",
+ "in": "query",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "query",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-validation-alias",
+ "/model-required-list-validation-alias",
+ ],
+)
+def test_required_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello&p=world")
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": ["hello", "world"]}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-list-validation-alias", "/model-required-list-validation-alias"],
+)
+def test_required_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/required-list-alias-and-validation-alias")
+def read_required_list_alias_and_validation_alias(
+ p: Annotated[List[str], Query(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class QueryModelRequiredListAliasAndValidationAlias(BaseModel):
+ p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.get("/model-required-list-alias-and-validation-alias")
+def read_model_required_list_alias_and_validation_alias(
+ p: Annotated[QueryModelRequiredListAliasAndValidationAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {
+ "title": "P Val Alias",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "name": "p_val_alias",
+ "in": "query",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "query",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello&p=world")
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "query",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {
+ "p": [
+ "hello",
+ "world",
+ ]
+ },
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello&p_alias=world")
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {"p_alias": ["hello", "world"]},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-list-alias-and-validation-alias",
+ "/model-required-list-alias-and-validation-alias",
+ ],
+)
+def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
diff --git a/tests/test_request_params/test_query/test_optional_list.py b/tests/test_request_params/test_query/test_optional_list.py
new file mode 100644
index 000000000..560921336
--- /dev/null
+++ b/tests/test_request_params/test_query/test_optional_list.py
@@ -0,0 +1,380 @@
+from typing import List, Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Query
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/optional-list-str")
+async def read_optional_list_str(
+ p: Annotated[Optional[List[str]], Query()] = None,
+):
+ return {"p": p}
+
+
+class QueryModelOptionalListStr(BaseModel):
+ p: Optional[List[str]] = None
+
+
+@app.get("/model-optional-list-str")
+async def read_model_optional_list_str(
+ p: Annotated[QueryModelOptionalListStr, Query()],
+):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P",
+ },
+ "name": "p",
+ "in": "query",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
+ "name": "p",
+ "in": "query",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-str", "/model-optional-list-str"],
+)
+def test_optional_list_str(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello&p=world")
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/optional-list-alias")
+async def read_optional_list_alias(
+ p: Annotated[Optional[List[str]], Query(alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class QueryModelOptionalListAlias(BaseModel):
+ p: Optional[List[str]] = Field(None, alias="p_alias")
+
+
+@app.get("/model-optional-list-alias")
+async def read_model_optional_list_alias(
+ p: Annotated[QueryModelOptionalListAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Alias",
+ },
+ "name": "p_alias",
+ "in": "query",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "P Alias",
+ },
+ "name": "p_alias",
+ "in": "query",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-alias", "/model-optional-list-alias"],
+)
+def test_optional_list_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello&p=world")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias",
+ "/model-optional-list-alias",
+ ],
+)
+def test_optional_list_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello&p_alias=world")
+ assert response.status_code == 200
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/optional-list-validation-alias")
+def read_optional_list_validation_alias(
+ p: Annotated[Optional[List[str]], Query(validation_alias="p_val_alias")] = None,
+):
+ return {"p": p}
+
+
+class QueryModelOptionalListValidationAlias(BaseModel):
+ p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
+
+
+@app.get("/model-optional-list-validation-alias")
+def read_model_optional_list_validation_alias(
+ p: Annotated[QueryModelOptionalListValidationAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "query",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-validation-alias",
+ "/model-optional-list-validation-alias",
+ ],
+)
+def test_optional_list_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello&p=world")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
+)
+def test_optional_list_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": ["hello", "world"]}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/optional-list-alias-and-validation-alias")
+def read_optional_list_alias_and_validation_alias(
+ p: Annotated[
+ Optional[List[str]], Query(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class QueryModelOptionalListAliasAndValidationAlias(BaseModel):
+ p: Optional[List[str]] = Field(
+ None, alias="p_alias", validation_alias="p_val_alias"
+ )
+
+
+@app.get("/model-optional-list-alias-and-validation-alias")
+def read_model_optional_list_alias_and_validation_alias(
+ p: Annotated[QueryModelOptionalListAliasAndValidationAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [
+ {"items": {"type": "string"}, "type": "array"},
+ {"type": "null"},
+ ],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "query",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello&p=world")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello&p_alias=world")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-list-alias-and-validation-alias",
+ "/model-optional-list-alias-and-validation-alias",
+ ],
+)
+def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "p": [
+ "hello",
+ "world",
+ ]
+ }
diff --git a/tests/test_request_params/test_query/test_optional_str.py b/tests/test_request_params/test_query/test_optional_str.py
new file mode 100644
index 000000000..25e4ea42e
--- /dev/null
+++ b/tests/test_request_params/test_query/test_optional_str.py
@@ -0,0 +1,354 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Query
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/optional-str")
+async def read_optional_str(p: Optional[str] = None):
+ return {"p": p}
+
+
+class QueryModelOptionalStr(BaseModel):
+ p: Optional[str] = None
+
+
+@app.get("/model-optional-str")
+async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P",
+ },
+ "name": "p",
+ "in": "query",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {"title": "P", "type": "string"},
+ "name": "p",
+ "in": "query",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-str", "/model-optional-str"],
+)
+def test_optional_str(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/optional-alias")
+async def read_optional_alias(
+ p: Annotated[Optional[str], Query(alias="p_alias")] = None,
+):
+ return {"p": p}
+
+
+class QueryModelOptionalAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias")
+
+
+@app.get("/model-optional-alias")
+async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ IsDict(
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Alias",
+ },
+ "name": "p_alias",
+ "in": "query",
+ }
+ )
+ | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "required": False,
+ "schema": {"title": "P Alias", "type": "string"},
+ "name": "p_alias",
+ "in": "query",
+ }
+ )
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-alias", "/model-optional-alias"],
+)
+def test_optional_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias",
+ "/model-optional-alias",
+ ],
+)
+def test_optional_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/optional-validation-alias")
+def read_optional_validation_alias(
+ p: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None,
+):
+ return {"p": p}
+
+
+class QueryModelOptionalValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, validation_alias="p_val_alias")
+
+
+@app.get("/model-optional-validation-alias")
+def read_model_optional_validation_alias(
+ p: Annotated[QueryModelOptionalValidationAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "query",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/optional-validation-alias", "/model-optional-validation-alias"],
+)
+def test_optional_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-validation-alias",
+ "/model-optional-validation-alias",
+ ],
+)
+def test_optional_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_val_alias=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/optional-alias-and-validation-alias")
+def read_optional_alias_and_validation_alias(
+ p: Annotated[
+ Optional[str], Query(alias="p_alias", validation_alias="p_val_alias")
+ ] = None,
+):
+ return {"p": p}
+
+
+class QueryModelOptionalAliasAndValidationAlias(BaseModel):
+ p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.get("/model-optional-alias-and-validation-alias")
+def read_model_optional_alias_and_validation_alias(
+ p: Annotated[QueryModelOptionalAliasAndValidationAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": False,
+ "schema": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "P Val Alias",
+ },
+ "name": "p_val_alias",
+ "in": "query",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": None}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/optional-alias-and-validation-alias",
+ "/model-optional-alias-and-validation-alias",
+ ],
+)
+def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_val_alias=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_request_params/test_query/test_required_str.py b/tests/test_request_params/test_query/test_required_str.py
new file mode 100644
index 000000000..aa0731e2c
--- /dev/null
+++ b/tests/test_request_params/test_query/test_required_str.py
@@ -0,0 +1,454 @@
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import FastAPI, Query
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+from tests.utils import needs_pydanticv2
+
+app = FastAPI()
+
+# =====================================================================================
+# Without aliases
+
+
+@app.get("/required-str")
+async def read_required_str(p: str):
+ return {"p": p}
+
+
+class QueryModelRequiredStr(BaseModel):
+ p: str
+
+
+@app.get("/model-required-str")
+async def read_model_required_str(p: Annotated[QueryModelRequiredStr, Query()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P", "type": "string"},
+ "name": "p",
+ "in": "query",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "p"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-str", "/model-required-str"],
+)
+def test_required_str(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello")
+ assert response.status_code == 200
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias
+
+
+@app.get("/required-alias")
+async def read_required_alias(p: Annotated[str, Query(alias="p_alias")]):
+ return {"p": p}
+
+
+class QueryModelRequiredAlias(BaseModel):
+ p: str = Field(alias="p_alias")
+
+
+@app.get("/model-required-alias")
+async def read_model_required_alias(p: Annotated[QueryModelRequiredAlias, Query()]):
+ return {"p": p.p}
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_str_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Alias", "type": "string"},
+ "name": "p_alias",
+ "in": "query",
+ }
+ ]
+
+
+@pytest.mark.parametrize(
+ "path",
+ ["/required-alias", "/model-required-alias"],
+)
+def test_required_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias",
+ "/model-required-alias",
+ ],
+)
+def test_required_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello")
+ assert response.status_code == 422
+ assert response.json() == IsDict(
+ {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {"p": "hello"},
+ ),
+ }
+ ]
+ }
+ ) | IsDict(
+ # TODO: remove when deprecating Pydantic v1
+ {
+ "detail": [
+ {
+ "loc": ["query", "p_alias"],
+ "msg": "field required",
+ "type": "value_error.missing",
+ }
+ ]
+ }
+ )
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias",
+ "/model-required-alias",
+ ],
+)
+def test_required_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Validation alias
+
+
+@app.get("/required-validation-alias")
+def read_required_validation_alias(
+ p: Annotated[str, Query(validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class QueryModelRequiredValidationAlias(BaseModel):
+ p: str = Field(validation_alias="p_val_alias")
+
+
+@app.get("/model-required-validation-alias")
+def read_model_required_validation_alias(
+ p: Annotated[QueryModelRequiredValidationAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ ["/required-validation-alias", "/model-required-validation-alias"],
+)
+def test_required_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Val Alias", "type": "string"},
+ "name": "p_val_alias",
+ "in": "query",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "query",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello")
+ assert response.status_code == 422, response.text
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-validation-alias",
+ "/model-required-validation-alias",
+ ],
+)
+def test_required_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_val_alias=hello")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
+
+
+# =====================================================================================
+# Alias and validation alias
+
+
+@app.get("/required-alias-and-validation-alias")
+def read_required_alias_and_validation_alias(
+ p: Annotated[str, Query(alias="p_alias", validation_alias="p_val_alias")],
+):
+ return {"p": p}
+
+
+class QueryModelRequiredAliasAndValidationAlias(BaseModel):
+ p: str = Field(alias="p_alias", validation_alias="p_val_alias")
+
+
+@app.get("/model-required-alias-and-validation-alias")
+def read_model_required_alias_and_validation_alias(
+ p: Annotated[QueryModelRequiredAliasAndValidationAlias, Query()],
+):
+ return {"p": p.p}
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_schema(path: str):
+ assert app.openapi()["paths"][path]["get"]["parameters"] == [
+ {
+ "required": True,
+ "schema": {"title": "P Val Alias", "type": "string"},
+ "name": "p_val_alias",
+ "in": "query",
+ }
+ ]
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_missing(path: str):
+ client = TestClient(app)
+ response = client.get(path)
+ assert response.status_code == 422
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "query",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_name(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p=hello")
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": [
+ "query",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {"p": "hello"},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello")
+ assert response.status_code == 422
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ {"p_alias": "hello"},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/required-alias-and-validation-alias",
+ "/model-required-alias-and-validation-alias",
+ ],
+)
+def test_required_alias_and_validation_alias_by_validation_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_val_alias=hello")
+ assert response.status_code == 200, response.text
+
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_union_body_discriminator_annotated.py b/tests/test_union_body_discriminator_annotated.py
new file mode 100644
index 000000000..14145e6f6
--- /dev/null
+++ b/tests/test_union_body_discriminator_annotated.py
@@ -0,0 +1,207 @@
+# Ref: https://github.com/fastapi/fastapi/discussions/14495
+
+from typing import Union
+
+import pytest
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from pydantic import BaseModel
+from typing_extensions import Annotated
+
+from .utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def client_fixture() -> TestClient:
+ from fastapi import Body
+ from pydantic import Discriminator, Tag
+
+ class Cat(BaseModel):
+ pet_type: str = "cat"
+ meows: int
+
+ class Dog(BaseModel):
+ pet_type: str = "dog"
+ barks: float
+
+ def get_pet_type(v):
+ assert isinstance(v, dict)
+ return v.get("pet_type", "")
+
+ Pet = Annotated[
+ Union[Annotated[Cat, Tag("cat")], Annotated[Dog, Tag("dog")]],
+ Discriminator(get_pet_type),
+ ]
+
+ app = FastAPI()
+
+ @app.post("/pet/assignment")
+ async def create_pet_assignment(pet: Pet = Body()):
+ return pet
+
+ @app.post("/pet/annotated")
+ async def create_pet_annotated(pet: Annotated[Pet, Body()]):
+ return pet
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+def test_union_body_discriminator_assignment(client: TestClient) -> None:
+ response = client.post("/pet/assignment", json={"pet_type": "cat", "meows": 5})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"pet_type": "cat", "meows": 5}
+
+
+@needs_pydanticv2
+def test_union_body_discriminator_annotated(client: TestClient) -> None:
+ response = client.post("/pet/annotated", json={"pet_type": "dog", "barks": 3.5})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"pet_type": "dog", "barks": 3.5}
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient) -> None:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == snapshot(
+ {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/pet/assignment": {
+ "post": {
+ "summary": "Create Pet Assignment",
+ "operationId": "create_pet_assignment_pet_assignment_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "anyOf": [
+ {"$ref": "#/components/schemas/Cat"},
+ {"$ref": "#/components/schemas/Dog"},
+ ],
+ "title": "Pet",
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/pet/annotated": {
+ "post": {
+ "summary": "Create Pet Annotated",
+ "operationId": "create_pet_annotated_pet_annotated_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {"$ref": "#/components/schemas/Cat"},
+ {"$ref": "#/components/schemas/Dog"},
+ ],
+ "title": "Pet",
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Cat": {
+ "properties": {
+ "pet_type": {
+ "type": "string",
+ "title": "Pet Type",
+ "default": "cat",
+ },
+ "meows": {"type": "integer", "title": "Meows"},
+ },
+ "type": "object",
+ "required": ["meows"],
+ "title": "Cat",
+ },
+ "Dog": {
+ "properties": {
+ "pet_type": {
+ "type": "string",
+ "title": "Pet Type",
+ "default": "dog",
+ },
+ "barks": {"type": "number", "title": "Barks"},
+ },
+ "type": "object",
+ "required": ["barks"],
+ "title": "Dog",
+ },
+ "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",
+ },
+ }
+ },
+ }
+ )