`ValidationError`.
-
-**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log.
-
-But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with an HTTP status code `500`.
-
-It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code.
-
-And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability.
-
### Override the `HTTPException` error handler { #override-the-httpexception-error-handler }
The same way, you can override the `HTTPException` handler.
For example, you could want to return a plain text response instead of JSON for these errors:
-{* ../../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 | Technical Details
@@ -188,6 +169,14 @@ You could also use `from starlette.responses import PlainTextResponse`.
///
+/// warning
+
+Have in mind that the `RequestValidationError` contains the information of the file name and line where the validation error happens so that you can show it in your logs with the relevant information if you want to.
+
+But that means that if you just convert it to a string and return that information directly, you could be leaking a bit of information about your system, that's why here the code extracts and shows each error independently.
+
+///
+
### Use the `RequestValidationError` body { #use-the-requestvalidationerror-body }
The `RequestValidationError` contains the `body` it received with invalid data.
diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md
index 3dcf5dc4a..c6a0e5b7d 100644
--- a/docs/en/docs/tutorial/testing.md
+++ b/docs/en/docs/tutorial/testing.md
@@ -121,63 +121,13 @@ It has a `POST` operation that could return several errors.
Both *path operations* require an `X-Token` header.
-//// 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+ non-Annotated
-
-/// tip
-
-Prefer to use the `Annotated` version if possible.
-
-///
-
-```Python
-{!> ../../docs_src/app_testing/app_b_py310/main.py!}
-```
-
-////
-
-//// tab | Python 3.8+ non-Annotated
-
-/// tip
-
-Prefer to use the `Annotated` version if possible.
-
-///
-
-```Python
-{!> ../../docs_src/app_testing/app_b/main.py!}
-```
-
-////
+{* ../../docs_src/app_testing/app_b_an_py310/main.py *}
### Extended testing file { #extended-testing-file }
You could then update `test_main.py` with the extended tests:
-{* ../../docs_src/app_testing/app_b/test_main.py *}
+{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *}
Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `httpx`, or even how to do it with `requests`, as HTTPX's design is based on Requests' design.
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index fd346a3d3..0e0adab9b 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -59,7 +59,6 @@ plugins:
search: null
macros:
include_yaml:
- - external_links: ../en/data/external_links.yml
- github_sponsors: ../en/data/github_sponsors.yml
- people: ../en/data/people.yml
- contributors: ../en/data/contributors.yml
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/docs_src/additional_responses/tutorial002_py310.py b/docs_src/additional_responses/tutorial002_py310.py
new file mode 100644
index 000000000..a94b740c9
--- /dev/null
+++ b/docs_src/additional_responses/tutorial002_py310.py
@@ -0,0 +1,28 @@
+from fastapi import FastAPI
+from fastapi.responses import FileResponse
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ id: str
+ value: str
+
+
+app = FastAPI()
+
+
+@app.get(
+ "/items/{item_id}",
+ response_model=Item,
+ responses={
+ 200: {
+ "content": {"image/png": {}},
+ "description": "Return the JSON item or an image.",
+ }
+ },
+)
+async def read_item(item_id: str, img: bool | None = None):
+ if img:
+ return FileResponse("image.png", media_type="image/png")
+ else:
+ return {"id": "foo", "value": "there goes my hero"}
diff --git a/docs_src/additional_responses/tutorial004_py310.py b/docs_src/additional_responses/tutorial004_py310.py
new file mode 100644
index 000000000..65cbef634
--- /dev/null
+++ b/docs_src/additional_responses/tutorial004_py310.py
@@ -0,0 +1,30 @@
+from fastapi import FastAPI
+from fastapi.responses import FileResponse
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ id: str
+ value: str
+
+
+responses = {
+ 404: {"description": "Item not found"},
+ 302: {"description": "The item was moved"},
+ 403: {"description": "Not enough privileges"},
+}
+
+
+app = FastAPI()
+
+
+@app.get(
+ "/items/{item_id}",
+ response_model=Item,
+ responses={**responses, 200: {"content": {"image/png": {}}}},
+)
+async def read_item(item_id: str, img: bool | None = None):
+ if img:
+ return FileResponse("image.png", media_type="image/png")
+ else:
+ return {"id": "foo", "value": "there goes my hero"}
diff --git a/docs_src/custom_request_and_route/tutorial001_an.py b/docs_src/custom_request_and_route/tutorial001_an.py
new file mode 100644
index 000000000..6224ba825
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial001_an.py
@@ -0,0 +1,36 @@
+import gzip
+from typing import Callable, List
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+from typing_extensions import Annotated
+
+
+class GzipRequest(Request):
+ async def body(self) -> bytes:
+ if not hasattr(self, "_body"):
+ body = await super().body()
+ if "gzip" in self.headers.getlist("Content-Encoding"):
+ body = gzip.decompress(body)
+ self._body = body
+ return self._body
+
+
+class GzipRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ request = GzipRequest(request.scope, request.receive)
+ return await original_route_handler(request)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: Annotated[List[int], Body()]):
+ return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial001_an_py310.py b/docs_src/custom_request_and_route/tutorial001_an_py310.py
new file mode 100644
index 000000000..381bab6d8
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial001_an_py310.py
@@ -0,0 +1,36 @@
+import gzip
+from collections.abc import Callable
+from typing import Annotated
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class GzipRequest(Request):
+ async def body(self) -> bytes:
+ if not hasattr(self, "_body"):
+ body = await super().body()
+ if "gzip" in self.headers.getlist("Content-Encoding"):
+ body = gzip.decompress(body)
+ self._body = body
+ return self._body
+
+
+class GzipRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ request = GzipRequest(request.scope, request.receive)
+ return await original_route_handler(request)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: Annotated[list[int], Body()]):
+ return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial001_an_py39.py b/docs_src/custom_request_and_route/tutorial001_an_py39.py
new file mode 100644
index 000000000..076727e64
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial001_an_py39.py
@@ -0,0 +1,35 @@
+import gzip
+from typing import Annotated, Callable
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class GzipRequest(Request):
+ async def body(self) -> bytes:
+ if not hasattr(self, "_body"):
+ body = await super().body()
+ if "gzip" in self.headers.getlist("Content-Encoding"):
+ body = gzip.decompress(body)
+ self._body = body
+ return self._body
+
+
+class GzipRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ request = GzipRequest(request.scope, request.receive)
+ return await original_route_handler(request)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: Annotated[list[int], Body()]):
+ return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial001_py310.py b/docs_src/custom_request_and_route/tutorial001_py310.py
new file mode 100644
index 000000000..c678088ce
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial001_py310.py
@@ -0,0 +1,35 @@
+import gzip
+from collections.abc import Callable
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class GzipRequest(Request):
+ async def body(self) -> bytes:
+ if not hasattr(self, "_body"):
+ body = await super().body()
+ if "gzip" in self.headers.getlist("Content-Encoding"):
+ body = gzip.decompress(body)
+ self._body = body
+ return self._body
+
+
+class GzipRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ request = GzipRequest(request.scope, request.receive)
+ return await original_route_handler(request)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: list[int] = Body()):
+ return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial001_py39.py b/docs_src/custom_request_and_route/tutorial001_py39.py
new file mode 100644
index 000000000..54b20b942
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial001_py39.py
@@ -0,0 +1,35 @@
+import gzip
+from typing import Callable
+
+from fastapi import Body, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class GzipRequest(Request):
+ async def body(self) -> bytes:
+ if not hasattr(self, "_body"):
+ body = await super().body()
+ if "gzip" in self.headers.getlist("Content-Encoding"):
+ body = gzip.decompress(body)
+ self._body = body
+ return self._body
+
+
+class GzipRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ request = GzipRequest(request.scope, request.receive)
+ return await original_route_handler(request)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = GzipRoute
+
+
+@app.post("/sum")
+async def sum_numbers(numbers: list[int] = Body()):
+ return {"sum": sum(numbers)}
diff --git a/docs_src/custom_request_and_route/tutorial002_an.py b/docs_src/custom_request_and_route/tutorial002_an.py
new file mode 100644
index 000000000..127f7a9ce
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial002_an.py
@@ -0,0 +1,30 @@
+from typing import Callable, List
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+from typing_extensions import Annotated
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ try:
+ return await original_route_handler(request)
+ except RequestValidationError as exc:
+ body = await request.body()
+ detail = {"errors": exc.errors(), "body": body.decode()}
+ raise HTTPException(status_code=422, detail=detail)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: Annotated[List[int], Body()]):
+ return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial002_an_py310.py b/docs_src/custom_request_and_route/tutorial002_an_py310.py
new file mode 100644
index 000000000..69b7de485
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial002_an_py310.py
@@ -0,0 +1,30 @@
+from collections.abc import Callable
+from typing import Annotated
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ try:
+ return await original_route_handler(request)
+ except RequestValidationError as exc:
+ body = await request.body()
+ detail = {"errors": exc.errors(), "body": body.decode()}
+ raise HTTPException(status_code=422, detail=detail)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: Annotated[list[int], Body()]):
+ return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial002_an_py39.py b/docs_src/custom_request_and_route/tutorial002_an_py39.py
new file mode 100644
index 000000000..e7de09de4
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial002_an_py39.py
@@ -0,0 +1,29 @@
+from typing import Annotated, Callable
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ try:
+ return await original_route_handler(request)
+ except RequestValidationError as exc:
+ body = await request.body()
+ detail = {"errors": exc.errors(), "body": body.decode()}
+ raise HTTPException(status_code=422, detail=detail)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: Annotated[list[int], Body()]):
+ return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial002_py310.py b/docs_src/custom_request_and_route/tutorial002_py310.py
new file mode 100644
index 000000000..13a5ca542
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial002_py310.py
@@ -0,0 +1,29 @@
+from collections.abc import Callable
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ try:
+ return await original_route_handler(request)
+ except RequestValidationError as exc:
+ body = await request.body()
+ detail = {"errors": exc.errors(), "body": body.decode()}
+ raise HTTPException(status_code=422, detail=detail)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: list[int] = Body()):
+ return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial002_py39.py b/docs_src/custom_request_and_route/tutorial002_py39.py
new file mode 100644
index 000000000..c4e474828
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial002_py39.py
@@ -0,0 +1,29 @@
+from typing import Callable
+
+from fastapi import Body, FastAPI, HTTPException, Request, Response
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+
+
+class ValidationErrorLoggingRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ try:
+ return await original_route_handler(request)
+ except RequestValidationError as exc:
+ body = await request.body()
+ detail = {"errors": exc.errors(), "body": body.decode()}
+ raise HTTPException(status_code=422, detail=detail)
+
+ return custom_route_handler
+
+
+app = FastAPI()
+app.router.route_class = ValidationErrorLoggingRoute
+
+
+@app.post("/")
+async def sum_numbers(numbers: list[int] = Body()):
+ return sum(numbers)
diff --git a/docs_src/custom_request_and_route/tutorial003_py310.py b/docs_src/custom_request_and_route/tutorial003_py310.py
new file mode 100644
index 000000000..f4e60be61
--- /dev/null
+++ b/docs_src/custom_request_and_route/tutorial003_py310.py
@@ -0,0 +1,39 @@
+import time
+from collections.abc import Callable
+
+from fastapi import APIRouter, FastAPI, Request, Response
+from fastapi.routing import APIRoute
+
+
+class TimedRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ before = time.time()
+ response: Response = await original_route_handler(request)
+ duration = time.time() - before
+ response.headers["X-Response-Time"] = str(duration)
+ print(f"route duration: {duration}")
+ print(f"route response: {response}")
+ print(f"route response headers: {response.headers}")
+ return response
+
+ return custom_route_handler
+
+
+app = FastAPI()
+router = APIRouter(route_class=TimedRoute)
+
+
+@app.get("/")
+async def not_timed():
+ return {"message": "Not timed"}
+
+
+@router.get("/timed")
+async def timed():
+ return {"message": "It's the time of my life"}
+
+
+app.include_router(router)
diff --git a/docs_src/dataclasses/tutorial001_py310.py b/docs_src/dataclasses/tutorial001_py310.py
new file mode 100644
index 000000000..ab709a7c8
--- /dev/null
+++ b/docs_src/dataclasses/tutorial001_py310.py
@@ -0,0 +1,19 @@
+from dataclasses import dataclass
+
+from fastapi import FastAPI
+
+
+@dataclass
+class Item:
+ name: str
+ price: float
+ description: str | None = None
+ tax: float | None = None
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+async def create_item(item: Item):
+ return item
diff --git a/docs_src/dataclasses/tutorial002_py310.py b/docs_src/dataclasses/tutorial002_py310.py
new file mode 100644
index 000000000..e16249f1e
--- /dev/null
+++ b/docs_src/dataclasses/tutorial002_py310.py
@@ -0,0 +1,25 @@
+from dataclasses import dataclass, field
+
+from fastapi import FastAPI
+
+
+@dataclass
+class Item:
+ name: str
+ price: float
+ tags: list[str] = field(default_factory=list)
+ description: str | None = None
+ tax: float | None = None
+
+
+app = FastAPI()
+
+
+@app.get("/items/next", response_model=Item)
+async def read_next_item():
+ return {
+ "name": "Island In The Moon",
+ "price": 12.99,
+ "description": "A place to be playin' and havin' fun",
+ "tags": ["breater"],
+ }
diff --git a/docs_src/dataclasses/tutorial002_py39.py b/docs_src/dataclasses/tutorial002_py39.py
new file mode 100644
index 000000000..0c23765d8
--- /dev/null
+++ b/docs_src/dataclasses/tutorial002_py39.py
@@ -0,0 +1,26 @@
+from dataclasses import dataclass, field
+from typing import Union
+
+from fastapi import FastAPI
+
+
+@dataclass
+class Item:
+ name: str
+ price: float
+ tags: list[str] = field(default_factory=list)
+ description: Union[str, None] = None
+ tax: Union[float, None] = None
+
+
+app = FastAPI()
+
+
+@app.get("/items/next", response_model=Item)
+async def read_next_item():
+ return {
+ "name": "Island In The Moon",
+ "price": 12.99,
+ "description": "A place to be playin' and havin' fun",
+ "tags": ["breater"],
+ }
diff --git a/docs_src/dataclasses/tutorial003_py310.py b/docs_src/dataclasses/tutorial003_py310.py
new file mode 100644
index 000000000..9b9a3fd63
--- /dev/null
+++ b/docs_src/dataclasses/tutorial003_py310.py
@@ -0,0 +1,54 @@
+from dataclasses import field # (1)
+
+from fastapi import FastAPI
+from pydantic.dataclasses import dataclass # (2)
+
+
+@dataclass
+class Item:
+ name: str
+ description: str | None = None
+
+
+@dataclass
+class Author:
+ name: str
+ items: list[Item] = field(default_factory=list) # (3)
+
+
+app = FastAPI()
+
+
+@app.post("/authors/{author_id}/items/", response_model=Author) # (4)
+async def create_author_items(author_id: str, items: list[Item]): # (5)
+ return {"name": author_id, "items": items} # (6)
+
+
+@app.get("/authors/", response_model=list[Author]) # (7)
+def get_authors(): # (8)
+ return [ # (9)
+ {
+ "name": "Breaters",
+ "items": [
+ {
+ "name": "Island In The Moon",
+ "description": "A place to be playin' and havin' fun",
+ },
+ {"name": "Holy Buddies"},
+ ],
+ },
+ {
+ "name": "System of an Up",
+ "items": [
+ {
+ "name": "Salt",
+ "description": "The kombucha mushroom people's favorite",
+ },
+ {"name": "Pad Thai"},
+ {
+ "name": "Lonely Night",
+ "description": "The mostests lonliest nightiest of allest",
+ },
+ ],
+ },
+ ]
diff --git a/docs_src/dataclasses/tutorial003_py39.py b/docs_src/dataclasses/tutorial003_py39.py
new file mode 100644
index 000000000..991708c00
--- /dev/null
+++ b/docs_src/dataclasses/tutorial003_py39.py
@@ -0,0 +1,55 @@
+from dataclasses import field # (1)
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic.dataclasses import dataclass # (2)
+
+
+@dataclass
+class Item:
+ name: str
+ description: Union[str, None] = None
+
+
+@dataclass
+class Author:
+ name: str
+ items: list[Item] = field(default_factory=list) # (3)
+
+
+app = FastAPI()
+
+
+@app.post("/authors/{author_id}/items/", response_model=Author) # (4)
+async def create_author_items(author_id: str, items: list[Item]): # (5)
+ return {"name": author_id, "items": items} # (6)
+
+
+@app.get("/authors/", response_model=list[Author]) # (7)
+def get_authors(): # (8)
+ return [ # (9)
+ {
+ "name": "Breaters",
+ "items": [
+ {
+ "name": "Island In The Moon",
+ "description": "A place to be playin' and havin' fun",
+ },
+ {"name": "Holy Buddies"},
+ ],
+ },
+ {
+ "name": "System of an Up",
+ "items": [
+ {
+ "name": "Salt",
+ "description": "The kombucha mushroom people's favorite",
+ },
+ {"name": "Pad Thai"},
+ {
+ "name": "Lonely Night",
+ "description": "The mostests lonliest nightiest of allest",
+ },
+ ],
+ },
+ ]
diff --git a/docs_src/handling_errors/tutorial004.py b/docs_src/handling_errors/tutorial004.py
index 300a3834f..ae50807e9 100644
--- a/docs_src/handling_errors/tutorial004.py
+++ b/docs_src/handling_errors/tutorial004.py
@@ -12,8 +12,11 @@ async def http_exception_handler(request, exc):
@app.exception_handler(RequestValidationError)
-async def validation_exception_handler(request, exc):
- return PlainTextResponse(str(exc), status_code=400)
+async def validation_exception_handler(request, exc: RequestValidationError):
+ message = "Validation errors:"
+ for error in exc.errors():
+ message += f"\nField: {error['loc']}, Error: {error['msg']}"
+ return PlainTextResponse(message, status_code=400)
@app.get("/items/{item_id}")
diff --git a/docs_src/openapi_callbacks/tutorial001_py310.py b/docs_src/openapi_callbacks/tutorial001_py310.py
new file mode 100644
index 000000000..3efe0ee25
--- /dev/null
+++ b/docs_src/openapi_callbacks/tutorial001_py310.py
@@ -0,0 +1,51 @@
+from fastapi import APIRouter, FastAPI
+from pydantic import BaseModel, HttpUrl
+
+app = FastAPI()
+
+
+class Invoice(BaseModel):
+ id: str
+ title: str | None = None
+ customer: str
+ total: float
+
+
+class InvoiceEvent(BaseModel):
+ description: str
+ paid: bool
+
+
+class InvoiceEventReceived(BaseModel):
+ ok: bool
+
+
+invoices_callback_router = APIRouter()
+
+
+@invoices_callback_router.post(
+ "{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived
+)
+def invoice_notification(body: InvoiceEvent):
+ pass
+
+
+@app.post("/invoices/", callbacks=invoices_callback_router.routes)
+def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None):
+ """
+ Create an invoice.
+
+ This will (let's imagine) let the API user (some external developer) create an
+ invoice.
+
+ And this path operation will:
+
+ * Send the invoice to the client.
+ * Collect the money from the client.
+ * Send a notification back to the API user (the external developer), as a callback.
+ * At this point is that the API will somehow send a POST request to the
+ external API with the notification of the invoice event
+ (e.g. "payment successful").
+ """
+ # Send the invoice, collect the money, send the notification (the callback)
+ return {"msg": "Invoice received"}
diff --git a/docs_src/path_operation_advanced_configuration/tutorial004_py310.py b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py
new file mode 100644
index 000000000..a815a564b
--- /dev/null
+++ b/docs_src/path_operation_advanced_configuration/tutorial004_py310.py
@@ -0,0 +1,28 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ description: str | None = None
+ price: float
+ tax: float | None = None
+ tags: set[str] = set()
+
+
+@app.post("/items/", response_model=Item, summary="Create an item")
+async def create_item(item: Item):
+ """
+ Create an item with all the information:
+
+ - **name**: each item must have a name
+ - **description**: a long description
+ - **price**: required
+ - **tax**: if the item doesn't have tax, you can omit this
+ - **tags**: a set of unique tag strings for this item
+ \f
+ :param item: User input.
+ """
+ return item
diff --git a/docs_src/path_operation_advanced_configuration/tutorial004_py39.py b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py
new file mode 100644
index 000000000..d5fe6705c
--- /dev/null
+++ b/docs_src/path_operation_advanced_configuration/tutorial004_py39.py
@@ -0,0 +1,30 @@
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ description: Union[str, None] = None
+ price: float
+ tax: Union[float, None] = None
+ tags: set[str] = set()
+
+
+@app.post("/items/", response_model=Item, summary="Create an item")
+async def create_item(item: Item):
+ """
+ Create an item with all the information:
+
+ - **name**: each item must have a name
+ - **description**: a long description
+ - **price**: required
+ - **tax**: if the item doesn't have tax, you can omit this
+ - **tags**: a set of unique tag strings for this item
+ \f
+ :param item: User input.
+ """
+ return item
diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py
new file mode 100644
index 000000000..831966553
--- /dev/null
+++ b/docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py
@@ -0,0 +1,32 @@
+import yaml
+from fastapi import FastAPI, HTTPException, Request
+from pydantic import BaseModel, ValidationError
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ tags: list[str]
+
+
+@app.post(
+ "/items/",
+ openapi_extra={
+ "requestBody": {
+ "content": {"application/x-yaml": {"schema": Item.schema()}},
+ "required": True,
+ },
+ },
+)
+async def create_item(request: Request):
+ raw_body = await request.body()
+ try:
+ data = yaml.safe_load(raw_body)
+ except yaml.YAMLError:
+ raise HTTPException(status_code=422, detail="Invalid YAML")
+ try:
+ item = Item.parse_obj(data)
+ except ValidationError as e:
+ raise HTTPException(status_code=422, detail=e.errors())
+ return item
diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_py39.py b/docs_src/path_operation_advanced_configuration/tutorial007_py39.py
new file mode 100644
index 000000000..ff64ef792
--- /dev/null
+++ b/docs_src/path_operation_advanced_configuration/tutorial007_py39.py
@@ -0,0 +1,32 @@
+import yaml
+from fastapi import FastAPI, HTTPException, Request
+from pydantic import BaseModel, ValidationError
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ tags: list[str]
+
+
+@app.post(
+ "/items/",
+ openapi_extra={
+ "requestBody": {
+ "content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
+ "required": True,
+ },
+ },
+)
+async def create_item(request: Request):
+ raw_body = await request.body()
+ try:
+ data = yaml.safe_load(raw_body)
+ except yaml.YAMLError:
+ raise HTTPException(status_code=422, detail="Invalid YAML")
+ try:
+ item = Item.model_validate(data)
+ except ValidationError as e:
+ raise HTTPException(status_code=422, detail=e.errors(include_url=False))
+ return item
diff --git a/docs_src/response_directly/tutorial001_py310.py b/docs_src/response_directly/tutorial001_py310.py
new file mode 100644
index 000000000..81e094dc6
--- /dev/null
+++ b/docs_src/response_directly/tutorial001_py310.py
@@ -0,0 +1,21 @@
+from datetime import datetime
+
+from fastapi import FastAPI
+from fastapi.encoders import jsonable_encoder
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ title: str
+ timestamp: datetime
+ description: str | None = None
+
+
+app = FastAPI()
+
+
+@app.put("/items/{id}")
+def update_item(id: str, item: Item):
+ json_compatible_item_data = jsonable_encoder(item)
+ return JSONResponse(content=json_compatible_item_data)
diff --git a/docs_src/settings/app03/config.py b/docs_src/settings/app03/config.py
index 942aea3e5..08f8f88c2 100644
--- a/docs_src/settings/app03/config.py
+++ b/docs_src/settings/app03/config.py
@@ -1,4 +1,4 @@
-from pydantic_settings import BaseSettings
+from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
@@ -6,5 +6,4 @@ class Settings(BaseSettings):
admin_email: str
items_per_user: int = 50
- class Config:
- env_file = ".env"
+ model_config = SettingsConfigDict(env_file=".env")
diff --git a/docs_src/settings/app03/config_pv1.py b/docs_src/settings/app03/config_pv1.py
new file mode 100644
index 000000000..e1c3ee300
--- /dev/null
+++ b/docs_src/settings/app03/config_pv1.py
@@ -0,0 +1,10 @@
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+ app_name: str = "Awesome API"
+ admin_email: str
+ items_per_user: int = 50
+
+ class Config:
+ env_file = ".env"
diff --git a/docs_src/settings/app03_an/main.py b/docs_src/settings/app03_an/main.py
index 2f64b9cd1..62f347639 100644
--- a/docs_src/settings/app03_an/main.py
+++ b/docs_src/settings/app03_an/main.py
@@ -1,7 +1,7 @@
from functools import lru_cache
-from typing import Annotated
from fastapi import Depends, FastAPI
+from typing_extensions import Annotated
from . import config
diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py
index 942aea3e5..08f8f88c2 100644
--- a/docs_src/settings/app03_an_py39/config.py
+++ b/docs_src/settings/app03_an_py39/config.py
@@ -1,4 +1,4 @@
-from pydantic_settings import BaseSettings
+from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
@@ -6,5 +6,4 @@ class Settings(BaseSettings):
admin_email: str
items_per_user: int = 50
- class Config:
- env_file = ".env"
+ model_config = SettingsConfigDict(env_file=".env")
diff --git a/docs_src/settings/app03_an_py39/config_pv1.py b/docs_src/settings/app03_an_py39/config_pv1.py
new file mode 100644
index 000000000..e1c3ee300
--- /dev/null
+++ b/docs_src/settings/app03_an_py39/config_pv1.py
@@ -0,0 +1,10 @@
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+ app_name: str = "Awesome API"
+ admin_email: str
+ items_per_user: int = 50
+
+ class Config:
+ env_file = ".env"
diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py
index 62f347639..2f64b9cd1 100644
--- a/docs_src/settings/app03_an_py39/main.py
+++ b/docs_src/settings/app03_an_py39/main.py
@@ -1,7 +1,7 @@
from functools import lru_cache
+from typing import Annotated
from fastapi import Depends, FastAPI
-from typing_extensions import Annotated
from . import config
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index 7009a7777..8de426ad4 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.0"
+__version__ = "0.124.2"
from starlette import status as status
diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py
index acd23d846..46a30b3ee 100644
--- a/fastapi/_compat/v2.py
+++ b/fastapi/_compat/v2.py
@@ -1,7 +1,7 @@
import re
import warnings
from copy import copy, deepcopy
-from dataclasses import dataclass
+from dataclasses import dataclass, is_dataclass
from enum import Enum
from typing import (
Any,
@@ -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, TypeAdapter, create_model
+from pydantic import BaseModel, ConfigDict, TypeAdapter, create_model
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
from pydantic import ValidationError as ValidationError
@@ -64,6 +64,7 @@ class ModelField:
field_info: FieldInfo
name: str
mode: Literal["validation", "serialization"] = "validation"
+ config: Union[ConfigDict, None] = None
@property
def alias(self) -> str:
@@ -94,8 +95,14 @@ class ModelField:
warnings.simplefilter(
"ignore", category=UnsupportedFieldAttributeWarning
)
+ annotated_args = (
+ self.field_info.annotation,
+ *self.field_info.metadata,
+ self.field_info,
+ )
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
- Annotated[self.field_info.annotation, self.field_info]
+ Annotated[annotated_args],
+ config=self.config,
)
def get_default(self) -> Any:
@@ -412,10 +419,21 @@ def create_body_model(
def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
- return [
- ModelField(field_info=field_info, name=name)
- for name, field_info in model.model_fields.items()
- ]
+ model_fields: List[ModelField] = []
+ for name, field_info in model.model_fields.items():
+ type_ = field_info.annotation
+ if lenient_issubclass(type_, (BaseModel, dict)) or is_dataclass(type_):
+ model_config = None
+ else:
+ model_config = model.model_config
+ model_fields.append(
+ ModelField(
+ field_info=field_info,
+ name=name,
+ config=model_config,
+ )
+ )
+ return model_fields
# Duplicate of several schema functions from Pydantic v1 to make them compatible with
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 23bca6f2a..262dba6fd 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -209,11 +209,21 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
return path_params + query_params + header_params + cookie_params
-def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
+def _get_signature(call: Callable[..., Any]) -> inspect.Signature:
if sys.version_info >= (3, 10):
- signature = inspect.signature(call, eval_str=True)
+ try:
+ signature = inspect.signature(call, eval_str=True)
+ except NameError:
+ # Handle type annotations with if TYPE_CHECKING, not used by FastAPI
+ # e.g. dependency return types
+ signature = inspect.signature(call)
else:
signature = inspect.signature(call)
+ return signature
+
+
+def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
+ signature = _get_signature(call)
unwrapped = inspect.unwrap(call)
globalns = getattr(unwrapped, "__globals__", {})
typed_params = [
@@ -239,10 +249,7 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
- if sys.version_info >= (3, 10):
- signature = inspect.signature(call, eval_str=True)
- else:
- signature = inspect.signature(call)
+ signature = _get_signature(call)
unwrapped = inspect.unwrap(call)
annotation = signature.return_annotation
diff --git a/pyproject.toml b/pyproject.toml
index cafcf65c6..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 = [
@@ -236,8 +237,15 @@ ignore = [
"docs_src/custom_response/tutorial007.py" = ["B007"]
"docs_src/dataclasses/tutorial003.py" = ["I001"]
"docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"]
+"docs_src/path_operation_advanced_configuration/tutorial007_py39.py" = ["B904"]
"docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"]
+"docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py" = ["B904"]
"docs_src/custom_request_and_route/tutorial002.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_py39.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_py310.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_an.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_an_py39.py" = ["B904"]
+"docs_src/custom_request_and_route/tutorial002_an_py310.py" = ["B904"]
"docs_src/dependencies/tutorial008_an.py" = ["F821"]
"docs_src/dependencies/tutorial008_an_py39.py" = ["F821"]
"docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"]
diff --git a/tests/test_arbitrary_types.py b/tests/test_arbitrary_types.py
new file mode 100644
index 000000000..e5fa95ef2
--- /dev/null
+++ b/tests/test_arbitrary_types.py
@@ -0,0 +1,141 @@
+from typing import List
+
+import pytest
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from typing_extensions import Annotated
+
+from .utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from pydantic import (
+ BaseModel,
+ ConfigDict,
+ PlainSerializer,
+ TypeAdapter,
+ WithJsonSchema,
+ )
+
+ class FakeNumpyArray:
+ def __init__(self):
+ self.data = [1.0, 2.0, 3.0]
+
+ FakeNumpyArrayPydantic = Annotated[
+ FakeNumpyArray,
+ WithJsonSchema(TypeAdapter(List[float]).json_schema()),
+ PlainSerializer(lambda v: v.data),
+ ]
+
+ class MyModel(BaseModel):
+ model_config = ConfigDict(arbitrary_types_allowed=True)
+ custom_field: FakeNumpyArrayPydantic
+
+ app = FastAPI()
+
+ @app.get("/")
+ def test() -> MyModel:
+ return MyModel(custom_field=FakeNumpyArray())
+
+ client = TestClient(app)
+ return client
+
+
+@needs_pydanticv2
+def test_get(client: TestClient):
+ response = client.get("/")
+ assert response.json() == {"custom_field": [1.0, 2.0, 3.0]}
+
+
+@needs_pydanticv2
+def test_typeadapter():
+ # This test is only to confirm that Pydantic alone is working as expected
+ from pydantic import (
+ BaseModel,
+ ConfigDict,
+ PlainSerializer,
+ TypeAdapter,
+ WithJsonSchema,
+ )
+
+ class FakeNumpyArray:
+ def __init__(self):
+ self.data = [1.0, 2.0, 3.0]
+
+ FakeNumpyArrayPydantic = Annotated[
+ FakeNumpyArray,
+ WithJsonSchema(TypeAdapter(List[float]).json_schema()),
+ PlainSerializer(lambda v: v.data),
+ ]
+
+ class MyModel(BaseModel):
+ model_config = ConfigDict(arbitrary_types_allowed=True)
+ custom_field: FakeNumpyArrayPydantic
+
+ ta = TypeAdapter(MyModel)
+ assert ta.dump_python(MyModel(custom_field=FakeNumpyArray())) == {
+ "custom_field": [1.0, 2.0, 3.0]
+ }
+ assert ta.json_schema() == snapshot(
+ {
+ "properties": {
+ "custom_field": {
+ "items": {"type": "number"},
+ "title": "Custom Field",
+ "type": "array",
+ }
+ },
+ "required": ["custom_field"],
+ "title": "MyModel",
+ "type": "object",
+ }
+ )
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+ response = client.get("openapi.json")
+ assert response.json() == snapshot(
+ {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "get": {
+ "summary": "Test",
+ "operationId": "test__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MyModel"
+ }
+ }
+ },
+ }
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "MyModel": {
+ "properties": {
+ "custom_field": {
+ "items": {"type": "number"},
+ "type": "array",
+ "title": "Custom Field",
+ }
+ },
+ "type": "object",
+ "required": ["custom_field"],
+ "title": "MyModel",
+ }
+ }
+ },
+ }
+ )
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..884e1d08a
--- /dev/null
+++ b/tests/test_request_params/test_body/test_list.py
@@ -0,0 +1,523 @@
+from typing import List, Union
+
+import pytest
+from dirty_equals import IsDict, IsOneOf, IsPartialDict
+from fastapi import Body, FastAPI
+from fastapi._compat import PYDANTIC_V2
+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} # pragma: no cover
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 models",
+ strict=False,
+ ),
+ ),
+ "/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} # pragma: no cover
+
+
+@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",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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( # /required-validation-alias fails here
+ ["body"], ["body", "p_val_alias"]
+ ),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-list-validation-alias fails here
+ )
+
+ 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",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-list-validation-alias fails here
+ )
+ 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} # pragma: no cover
+
+
+@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",
+ [
+ pytest.param(
+ "/required-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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( # /required-list-alias-and-validation-alias fails here
+ ["body"], ["body", "p_val_alias"]
+ ),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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": [ # /required-list-alias-and-validation-alias fails here
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": ["hello", "world"]}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/required-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-list-alias-and-validation-alias fails here
+ )
+ 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..c86398ce9
--- /dev/null
+++ b/tests/test_request_params/test_body/test_optional_list.py
@@ -0,0 +1,600 @@
+from typing import List, Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import Body, FastAPI
+from fastapi._compat import PYDANTIC_V2
+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",
+ [
+ pytest.param(
+ "/optional-list-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ strict=False,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ ),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/optional-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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} # /optional-list-validation-alias fails here
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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() == { # /optional-list-validation-alias fails here
+ "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",
+ [
+ pytest.param(
+ "/optional-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-list-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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": [ # /optional-list-alias-and-validation-alias fails here
+ "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..43ed367dd
--- /dev/null
+++ b/tests/test_request_params/test_body/test_optional_str.py
@@ -0,0 +1,569 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import Body, FastAPI
+from fastapi._compat import PYDANTIC_V2
+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",
+ [
+ pytest.param(
+ "/optional-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ strict=False,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ ),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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} # /optional-validation-alias fails here
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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"} # /optional-validation-alias fails here
+
+
+# =====================================================================================
+# 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",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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" # /optional-alias-and-validation-alias fails here
+ }
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..fba3fe1f6
--- /dev/null
+++ b/tests/test_request_params/test_body/test_required_str.py
@@ -0,0 +1,514 @@
+from typing import Any, Dict, Union
+
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import Body, FastAPI
+from fastapi._compat import PYDANTIC_V2
+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",
+ [
+ pytest.param(
+ "/required-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ strict=False,
+ ),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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( # /required-validation-alias fails here
+ ["body", "p_val_alias"], ["body"]
+ ),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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( # /required-alias-and-validation-alias fails here
+ ["body"], ["body", "p_val_alias"]
+ ),
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-alias-and-validation-alias fails here
+ )
+
+ 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",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-alias-and-validation-alias fails here
+ )
+
+ 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..7298baacd
--- /dev/null
+++ b/tests/test_request_params/test_cookie/test_optional_str.py
@@ -0,0 +1,383 @@
+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",
+ pytest.param(
+ "/model-optional-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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"} # /model-optional-alias fails here
+
+
+# =====================================================================================
+# 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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"} # /optional-validation-alias fails here
+
+
+# =====================================================================================
+# 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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" # /optional-alias-and-validation-alias fails here
+ }
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..9c1442ccb
--- /dev/null
+++ b/tests/test_request_params/test_cookie/test_required_str.py
@@ -0,0 +1,503 @@
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import Cookie, FastAPI
+from fastapi._compat import PYDANTIC_V2
+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} # pragma: no cover
+
+
+@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",
+ pytest.param(
+ "/model-required-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 models",
+ strict=False,
+ ),
+ ),
+ ],
+)
+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"}, # /model-required-alias PDv2 fails here
+ ),
+ }
+ ]
+ }
+ ) | 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",
+ pytest.param(
+ "/model-required-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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, ( # /model-required-alias fails here
+ 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf( # /model-alias-and-validation-alias fails here
+ None,
+ {"p": "hello"},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /required-alias-and-validation-alias fails here
+ )
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["cookie", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf( # /model-alias-and-validation-alias fails here
+ None,
+ {"p_alias": "hello"},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-alias-and-validation-alias fails here
+ )
+
+ 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..8722ce5ab
--- /dev/null
+++ b/tests/test_request_params/test_file/test_list.py
@@ -0,0 +1,597 @@
+from typing import List
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, File, UploadFile
+from fastapi._compat import PYDANTIC_V2
+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.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ strict=False,
+)
+@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",
+ [
+ pytest.param(
+ "/list-bytes-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/list-uploadfile-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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": [ # /list-*-validation-alias fail here
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/list-bytes-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/list-uploadfile-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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, ( # /list-*-validation-alias fail here
+ response.text
+ )
+
+ assert response.json() == { # pragma: no cover
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # all 2 fail here
+ assert response.json() == {"file_size": [5, 5]} # pragma: no cover
+
+
+# =====================================================================================
+# 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",
+ [
+ pytest.param(
+ "/list-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/list-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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", # /list-*-alias-and-validation-alias fail here
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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", # /list-*-alias-and-validation-alias fail here
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/list-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/list-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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 # /list-*-alias-and-validation-alias fails here
+ )
+
+ assert response.json() == { # pragma: no cover
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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, ( # all 2 fail here
+ response.text
+ )
+ assert response.json() == {"file_size": [5, 5]} # pragma: no cover
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..14fc0a220
--- /dev/null
+++ b/tests/test_request_params/test_file/test_optional.py
@@ -0,0 +1,443 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, File, UploadFile
+from fastapi._compat import PYDANTIC_V2
+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.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ strict=False,
+)
+@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",
+ [
+ pytest.param(
+ "/optional-bytes-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/optional-uploadfile-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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() == { # /optional-*-validation-alias fail here
+ "file_size": None
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-bytes-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/optional-uploadfile-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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} # /optional-*-validation-alias fail here
+
+
+# =====================================================================================
+# 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",
+ [
+ pytest.param(
+ "/optional-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/optional-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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",
+ [
+ pytest.param(
+ "/optional-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/optional-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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
+ } # /optional-*-alias-and-validation-alias fail here
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..f266642a6
--- /dev/null
+++ b/tests/test_request_params/test_file/test_optional_list.py
@@ -0,0 +1,487 @@
+from typing import List, Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, File, UploadFile
+from fastapi._compat import PYDANTIC_V2
+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",
+ [
+ pytest.param(
+ "/optional-list-bytes",
+ marks=pytest.mark.xfail(
+ raises=(TypeError, AssertionError),
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 due to #14297",
+ strict=False,
+ ),
+ ),
+ "/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.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ strict=False,
+)
+@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",
+ [
+ pytest.param(
+ "/optional-list-bytes-alias",
+ marks=pytest.mark.xfail(
+ raises=(TypeError, AssertionError),
+ strict=False,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 model due to #14297",
+ ),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/optional-list-bytes-validation-alias",
+ marks=pytest.mark.xfail(
+ raises=(TypeError, AssertionError),
+ strict=False,
+ reason="Fails due to #14297",
+ ),
+ ),
+ pytest.param(
+ "/optional-list-uploadfile-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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() == { # /optional-list-uploadfile-validation-alias fails here
+ "file_size": None
+ }
+
+
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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] # /optional-list-*-validation-alias fail here
+ }
+
+
+# =====================================================================================
+# 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",
+ [
+ pytest.param(
+ "/optional-list-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(
+ raises=(TypeError, AssertionError),
+ strict=False,
+ reason="Fails due to #14297",
+ ),
+ ),
+ pytest.param(
+ "/optional-list-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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 ( # /optional-list-uploadfile-alias-and-validation-alias fails here
+ response.json() == {"file_size": None}
+ )
+
+
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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] # /optional-list-*-alias-and-validation-alias fail here
+ }
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..e50597370
--- /dev/null
+++ b/tests/test_request_params/test_file/test_required.py
@@ -0,0 +1,536 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, File, UploadFile
+from fastapi._compat import PYDANTIC_V2
+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.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ strict=False,
+)
+@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",
+ [
+ pytest.param(
+ "/required-bytes-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/required-uploadfile-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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": [ # /required-*-validation-alias fail here
+ "body",
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-bytes-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/required-uploadfile-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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, ( # /required-*-validation-alias fail here
+ response.text
+ )
+
+ assert response.json() == { # pragma: no cover
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-bytes-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/required-uploadfile-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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, ( # all 2 fail here
+ response.text
+ )
+ assert response.json() == {"file_size": 5} # pragma: no cover
+
+
+# =====================================================================================
+# 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",
+ [
+ pytest.param(
+ "/required-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/required-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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", # /required-*-alias-and-validation-alias fail here
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/required-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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", # /required-*-alias-and-validation-alias fail here
+ ],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/required-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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 # /required-*-alias-and-validation-alias fails here
+ )
+
+ assert response.json() == { # pragma: no cover
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["body", "p_val_alias"],
+ "msg": "Field required",
+ "input": None,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-bytes-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ pytest.param(
+ "/required-uploadfile-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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, ( # all 2 fail here
+ response.text
+ )
+ assert response.json() == {"file_size": 5} # pragma: no cover
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..c57180f6a
--- /dev/null
+++ b/tests/test_request_params/test_form/test_list.py
@@ -0,0 +1,527 @@
+from typing import List
+
+import pytest
+from dirty_equals import IsDict, IsOneOf, IsPartialDict
+from fastapi import FastAPI, Form
+from fastapi._compat import PYDANTIC_V2
+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} # pragma: no cover
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 models",
+ strict=False,
+ ),
+ ),
+ "/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",
+ pytest.param(
+ "/model-required-list-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 models",
+ strict=False,
+ ),
+ ),
+ ],
+)
+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( # /model-required-list-alias with PDv2 fails here
+ 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} # pragma: no cover
+
+
+@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",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-list-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-list-validation-alias fails here
+ )
+
+ 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.xfail(raises=AssertionError, strict=False)
+@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 # both fail here
+
+ assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
+
+
+# =====================================================================================
+# 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} # pragma: no cover
+
+
+@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",
+ [
+ pytest.param(
+ "/required-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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",
+ # /required-list-alias-and-validation-alias fails here
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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",
+ # /required-list-alias-and-validation-alias fails here
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ # /model-required-list-alias-and-validation-alias fails here
+ {"p": ["hello", "world"]},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 ( # /required-list-alias-and-validation-alias fails here
+ 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.xfail(raises=AssertionError, strict=False)
+@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 # both fail here
+ assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
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..288a0cfe4
--- /dev/null
+++ b/tests/test_request_params/test_form/test_optional_list.py
@@ -0,0 +1,454 @@
+from typing import List, Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Form
+from fastapi._compat import PYDANTIC_V2
+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",
+ [
+ pytest.param(
+ "/optional-list-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ strict=False,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ ),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/optional-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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} # /optional-list-validation-alias fails here
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /model-optional-list-validation-alias fails here
+ )
+ assert response.json() == { # /optional-list-validation-alias fails here
+ "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",
+ [
+ pytest.param(
+ "/optional-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-list-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /model-optional-list-alias-and-validation-alias fails here
+ )
+ assert response.json() == {
+ "p": [ # /optional-list-alias-and-validation-alias fails here
+ "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..66c003a95
--- /dev/null
+++ b/tests/test_request_params/test_form/test_optional_str.py
@@ -0,0 +1,419 @@
+from typing import Optional
+
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Form
+from fastapi._compat import PYDANTIC_V2
+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",
+ [
+ pytest.param(
+ "/optional-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ strict=False,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ ),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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} # /optional-validation-alias fails here
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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"} # /optional-validation-alias fails here
+
+
+# =====================================================================================
+# 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",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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" # /optional-alias-and-validation-alias fails here
+ }
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..fcbce015d
--- /dev/null
+++ b/tests/test_request_params/test_form/test_required_str.py
@@ -0,0 +1,502 @@
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import FastAPI, Form
+from fastapi._compat import PYDANTIC_V2
+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",
+ [
+ pytest.param(
+ "/required-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2",
+ strict=False,
+ ),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": "hello"}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-alias-and-validation-alias fails here
+ )
+
+ 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",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-alias-and-validation-alias fails here
+ )
+
+ 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..1bd3628b8
--- /dev/null
+++ b/tests/test_request_params/test_header/test_list.py
@@ -0,0 +1,505 @@
+from typing import List
+
+import pytest
+from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
+from fastapi import FastAPI, Header
+from fastapi._compat import PYDANTIC_V2
+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} # pragma: no cover
+
+
+@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",
+ pytest.param(
+ "/model-required-list-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 models",
+ strict=False,
+ ),
+ ),
+ ],
+)
+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( # /model-required-list-alias with PDv2 fails here
+ 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",
+ pytest.param(
+ "/model-required-list-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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, ( # /model-required-list-alias fails here
+ 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} # pragma: no cover
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-list-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-list-validation-alias fails here
+
+ 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.xfail(raises=AssertionError, strict=False)
+@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 # both fail here
+
+ assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
+
+
+# =====================================================================================
+# 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} # pragma: no cover
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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",
+ # /required-list-alias-and-validation-alias fails here
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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",
+ # /required-list-alias-and-validation-alias fails here
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ # /model-required-list-alias-and-validation-alias fails here
+ IsPartialDict({"p": ["hello", "world"]}),
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 ( # /required-list-alias-and-validation-alias fails here
+ response.status_code == 422
+ )
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ # /model-required-list-alias-and-validation-alias fails here
+ IsPartialDict({"p_alias": ["hello", "world"]}),
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # both fail here
+ assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
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..328f039ba
--- /dev/null
+++ b/tests/test_request_params/test_header/test_optional_list.py
@@ -0,0 +1,407 @@
+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",
+ pytest.param(
+ "/model-optional-list-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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"] # /model-optional-list-alias fails here
+ }
+
+
+# =====================================================================================
+# 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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} # /optional-list-validation-alias fails here
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /model-optional-list-validation-alias fails here
+ )
+ assert response.json() == { # /optional-list-validation-alias fails here
+ "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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-list-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /model-optional-list-alias-and-validation-alias fails here
+ )
+ assert response.json() == {
+ "p": [ # /optional-list-alias-and-validation-alias fails here
+ "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..d63e0a2b8
--- /dev/null
+++ b/tests/test_request_params/test_header/test_optional_str.py
@@ -0,0 +1,375 @@
+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",
+ pytest.param(
+ "/model-optional-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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"} # /model-optional-alias fails here
+
+
+# =====================================================================================
+# 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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} # /optional-validation-alias fails here
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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"} # /optional-validation-alias fails here
+
+
+# =====================================================================================
+# 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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" # /optional-alias-and-validation-alias fails here
+ }
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..6eb4fd6f6
--- /dev/null
+++ b/tests/test_request_params/test_header/test_required_str.py
@@ -0,0 +1,492 @@
+import pytest
+from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
+from fastapi import FastAPI, Header
+from fastapi._compat import PYDANTIC_V2
+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} # pragma: no cover
+
+
+@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",
+ pytest.param(
+ "/model-required-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 models",
+ strict=False,
+ ),
+ ),
+ ],
+)
+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",
+ pytest.param(
+ "/model-required-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+def test_required_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(path, headers={"p_alias": "hello"})
+ assert response.status_code == 200, ( # /model-required-alias fails here
+ 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": AnyThing,
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf( # /model-alias-and-validation-alias fails here
+ None,
+ IsPartialDict({"p": "hello"}),
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /required-alias-and-validation-alias fails here
+ )
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["header", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf( # /model-alias-and-validation-alias fails here
+ None,
+ IsPartialDict({"p_alias": "hello"}),
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-alias-and-validation-alias fails here
+ )
+
+ 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..8e2e60004
--- /dev/null
+++ b/tests/test_request_params/test_path/test_required_str.py
@@ -0,0 +1,102 @@
+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} # pragma: no cover
+
+
+@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} # pragma: no cover
+
+
+@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.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ),
+ 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,
+ pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ),
+ ],
+)
+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.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ),
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ id="required-alias-and-validation-alias",
+ marks=(
+ needs_pydanticv2,
+ pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ),
+ ],
+)
+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..4edd192e0
--- /dev/null
+++ b/tests/test_request_params/test_query/test_list.py
@@ -0,0 +1,506 @@
+from typing import List
+
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import FastAPI, Query
+from fastapi._compat import PYDANTIC_V2
+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} # pragma: no cover
+
+
+@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",
+ pytest.param(
+ "/model-required-list-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 models",
+ strict=False,
+ ),
+ ),
+ ],
+)
+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( # /model-required-list-alias with PDv2 fails here
+ 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",
+ pytest.param(
+ "/model-required-list-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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, ( # /model-required-list-alias fails here
+ 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} # pragma: no cover
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-list-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-list-validation-alias fails here
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(None, {"p": ["hello", "world"]}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # both fail here
+
+ assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
+
+
+# =====================================================================================
+# 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} # pragma: no cover
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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",
+ # /required-list-alias-and-validation-alias fails here
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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",
+ # /required-list-alias-and-validation-alias fails here
+ "p_val_alias",
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ # /model-required-list-alias-and-validation-alias fails here
+ {
+ "p": [
+ "hello",
+ "world",
+ ]
+ },
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 ( # /required-list-alias-and-validation-alias fails here
+ response.status_code == 422
+ )
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf(
+ None,
+ # /model-required-list-alias-and-validation-alias fails here
+ {"p_alias": ["hello", "world"]},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # both fail here
+ assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
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..76f960554
--- /dev/null
+++ b/tests/test_request_params/test_query/test_optional_list.py
@@ -0,0 +1,403 @@
+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",
+ pytest.param(
+ "/model-optional-list-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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"] # /model-optional-list-alias fails here
+ }
+
+
+# =====================================================================================
+# 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-list-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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} # /optional-list-validation-alias fails here
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /model-optional-list-validation-alias fails here
+ )
+ assert response.json() == { # /optional-list-validation-alias fails here
+ "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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-list-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-list-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /model-optional-list-alias-and-validation-alias fails here
+ )
+ assert response.json() == {
+ "p": [ # /optional-list-alias-and-validation-alias fails here
+ "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..77da9bee6
--- /dev/null
+++ b/tests/test_request_params/test_query/test_optional_str.py
@@ -0,0 +1,375 @@
+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",
+ pytest.param(
+ "/model-optional-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+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"} # /model-optional-alias fails here
+
+
+# =====================================================================================
+# 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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",
+ [
+ pytest.param(
+ "/optional-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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"} # /optional-validation-alias fails here
+
+
+# =====================================================================================
+# 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /optional-alias-and-validation-alias fails here
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/optional-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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" # /optional-alias-and-validation-alias fails here
+ }
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..aa3a27683
--- /dev/null
+++ b/tests/test_request_params/test_query/test_required_str.py
@@ -0,0 +1,495 @@
+import pytest
+from dirty_equals import IsDict, IsOneOf
+from fastapi import FastAPI, Query
+from fastapi._compat import PYDANTIC_V2
+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} # pragma: no cover
+
+
+@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",
+ pytest.param(
+ "/model-required-alias",
+ marks=pytest.mark.xfail(
+ raises=AssertionError,
+ condition=PYDANTIC_V2,
+ reason="Fails only with PDv2 models",
+ strict=False,
+ ),
+ ),
+ ],
+)
+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"}, # /model-required-alias PDv2 fails here
+ ),
+ }
+ ]
+ }
+ ) | 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",
+ pytest.param(
+ "/model-required-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ ],
+)
+def test_required_alias_by_alias(path: str):
+ client = TestClient(app)
+ response = client.get(f"{path}?p_alias=hello")
+ assert response.status_code == 200, ( # /model-required-alias fails here
+ 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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",
+ [
+ pytest.param(
+ "/required-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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, ( # /required-validation-alias fails here
+ 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.xfail(raises=AssertionError, strict=False)
+@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",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf(None, {}),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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", # /required-alias-and-validation-alias fails here
+ ],
+ "msg": "Field required",
+ "input": IsOneOf( # /model-alias-and-validation-alias fails here
+ None,
+ {"p": "hello"},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.xfail(raises=AssertionError, strict=False)
+@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 # /required-alias-and-validation-alias fails here
+ )
+
+ assert response.json() == {
+ "detail": [
+ {
+ "type": "missing",
+ "loc": ["query", "p_val_alias"],
+ "msg": "Field required",
+ "input": IsOneOf( # /model-alias-and-validation-alias fails here
+ None,
+ {"p_alias": "hello"},
+ ),
+ }
+ ]
+ }
+
+
+@needs_pydanticv2
+@pytest.mark.parametrize(
+ "path",
+ [
+ pytest.param(
+ "/required-alias-and-validation-alias",
+ marks=pytest.mark.xfail(raises=AssertionError, strict=False),
+ ),
+ "/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 # /required-alias-and-validation-alias fails here
+ )
+
+ assert response.json() == {"p": "hello"}
diff --git a/tests/test_stringified_annotation_dependency.py b/tests/test_stringified_annotation_dependency.py
new file mode 100644
index 000000000..89bb884b5
--- /dev/null
+++ b/tests/test_stringified_annotation_dependency.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+from fastapi import Depends, FastAPI
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from typing_extensions import Annotated
+
+if TYPE_CHECKING: # pragma: no cover
+ from collections.abc import AsyncGenerator
+
+
+class DummyClient:
+ async def get_people(self) -> list:
+ return ["John Doe", "Jane Doe"]
+
+ async def close(self) -> None:
+ pass
+
+
+async def get_client() -> AsyncGenerator[DummyClient, None]:
+ client = DummyClient()
+ yield client
+ await client.close()
+
+
+Client = Annotated[DummyClient, Depends(get_client)]
+
+
+@pytest.fixture(name="client")
+def client_fixture() -> TestClient:
+ app = FastAPI()
+
+ @app.get("/")
+ async def get_people(client: Client) -> list:
+ return await client.get_people()
+
+ client = TestClient(app)
+ return client
+
+
+def test_get(client: TestClient):
+ response = client.get("/")
+ assert response.status_code == 200, response.text
+ assert response.json() == ["John Doe", "Jane Doe"]
+
+
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == snapshot(
+ {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "get": {
+ "summary": "Get People",
+ "operationId": "get_people__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {},
+ "type": "array",
+ "title": "Response Get People Get",
+ }
+ }
+ },
+ }
+ },
+ }
+ }
+ },
+ }
+ )
diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py
index 588a3160a..91d6ff101 100644
--- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py
+++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py
@@ -1,21 +1,36 @@
+import importlib
import os
import shutil
+import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from docs_src.additional_responses.tutorial002 import app
-
-client = TestClient(app)
+from tests.utils import needs_py310
-def test_path_operation():
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial002"),
+ pytest.param("tutorial002_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.additional_responses.{request.param}")
+
+ client = TestClient(mod.app)
+ client.headers.clear()
+ return client
+
+
+def test_path_operation(client: TestClient):
response = client.get("/items/foo")
assert response.status_code == 200, response.text
assert response.json() == {"id": "foo", "value": "there goes my hero"}
-def test_path_operation_img():
+def test_path_operation_img(client: TestClient):
shutil.copy("./docs/en/docs/img/favicon.png", "./image.png")
response = client.get("/items/foo?img=1")
assert response.status_code == 200, response.text
@@ -24,7 +39,7 @@ def test_path_operation_img():
os.remove("./image.png")
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py
index 55b556d8e..2d9491467 100644
--- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py
+++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py
@@ -1,21 +1,36 @@
+import importlib
import os
import shutil
+import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from docs_src.additional_responses.tutorial004 import app
-
-client = TestClient(app)
+from tests.utils import needs_py310
-def test_path_operation():
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial004"),
+ pytest.param("tutorial004_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.additional_responses.{request.param}")
+
+ client = TestClient(mod.app)
+ client.headers.clear()
+ return client
+
+
+def test_path_operation(client: TestClient):
response = client.get("/items/foo")
assert response.status_code == 200, response.text
assert response.json() == {"id": "foo", "value": "there goes my hero"}
-def test_path_operation_img():
+def test_path_operation_img(client: TestClient):
shutil.copy("./docs/en/docs/img/favicon.png", "./image.png")
response = client.get("/items/foo?img=1")
assert response.status_code == 200, response.text
@@ -24,7 +39,7 @@ def test_path_operation_img():
os.remove("./image.png")
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py
index e6da630e8..f9fd0d1af 100644
--- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py
+++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py
@@ -1,23 +1,38 @@
import gzip
+import importlib
import json
import pytest
from fastapi import Request
from fastapi.testclient import TestClient
-from docs_src.custom_request_and_route.tutorial001 import app
+from tests.utils import needs_py39, needs_py310
-@app.get("/check-class")
-async def check_gzip_request(request: Request):
- return {"request_class": type(request).__name__}
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial001"),
+ pytest.param("tutorial001_py39", marks=needs_py39),
+ pytest.param("tutorial001_py310", marks=needs_py310),
+ pytest.param("tutorial001_an"),
+ pytest.param("tutorial001_an_py39", marks=needs_py39),
+ pytest.param("tutorial001_an_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}")
+ @mod.app.get("/check-class")
+ async def check_gzip_request(request: Request):
+ return {"request_class": type(request).__name__}
-client = TestClient(app)
+ client = TestClient(mod.app)
+ return client
@pytest.mark.parametrize("compress", [True, False])
-def test_gzip_request(compress):
+def test_gzip_request(client: TestClient, compress):
n = 1000
headers = {}
body = [1] * n
@@ -30,6 +45,6 @@ def test_gzip_request(compress):
assert response.json() == {"sum": n}
-def test_request_class():
+def test_request_class(client: TestClient):
response = client.get("/check-class")
assert response.json() == {"request_class": "GzipRequest"}
diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
index 647f1c5dd..c35752ed1 100644
--- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
+++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
@@ -1,17 +1,36 @@
+import importlib
+
+import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi.testclient import TestClient
-from docs_src.custom_request_and_route.tutorial002 import app
-
-client = TestClient(app)
+from tests.utils import needs_py39, needs_py310
-def test_endpoint_works():
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial002"),
+ pytest.param("tutorial002_py39", marks=needs_py39),
+ pytest.param("tutorial002_py310", marks=needs_py310),
+ pytest.param("tutorial002_an"),
+ pytest.param("tutorial002_an_py39", marks=needs_py39),
+ pytest.param("tutorial002_an_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}")
+
+ client = TestClient(mod.app)
+ return client
+
+
+def test_endpoint_works(client: TestClient):
response = client.post("/", json=[1, 2, 3])
assert response.json() == 6
-def test_exception_handler_body_access():
+def test_exception_handler_body_access(client: TestClient):
response = client.post("/", json={"numbers": [1, 2, 3]})
assert response.json() == IsDict(
{
diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py
index db5dad7cf..9e895b2da 100644
--- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py
+++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial003.py
@@ -1,17 +1,32 @@
+import importlib
+
+import pytest
from fastapi.testclient import TestClient
-from docs_src.custom_request_and_route.tutorial003 import app
-
-client = TestClient(app)
+from tests.utils import needs_py310
-def test_get():
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial003"),
+ pytest.param("tutorial003_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}")
+
+ client = TestClient(mod.app)
+ return client
+
+
+def test_get(client: TestClient):
response = client.get("/")
assert response.json() == {"message": "Not timed"}
assert "X-Response-Time" not in response.headers
-def test_get_timed():
+def test_get_timed(client: TestClient):
response = client.get("/timed")
assert response.json() == {"message": "It's the time of my life"}
assert "X-Response-Time" in response.headers
diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py
index 762654d29..b36dee768 100644
--- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py
+++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py
@@ -1,12 +1,28 @@
+import importlib
+
+import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from docs_src.dataclasses.tutorial001 import app
-
-client = TestClient(app)
+from tests.utils import needs_py310
-def test_post_item():
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial001"),
+ pytest.param("tutorial001_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
+
+ client = TestClient(mod.app)
+ client.headers.clear()
+ return client
+
+
+def test_post_item(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "price": 3})
assert response.status_code == 200
assert response.json() == {
@@ -17,7 +33,7 @@ def test_post_item():
}
-def test_post_invalid_item():
+def test_post_invalid_item(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "price": "invalid price"})
assert response.status_code == 422
assert response.json() == IsDict(
@@ -45,7 +61,7 @@ def test_post_invalid_item():
)
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py
index e6d303cfc..baaea45d8 100644
--- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py
+++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py
@@ -1,12 +1,29 @@
+import importlib
+
+import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi.testclient import TestClient
-from docs_src.dataclasses.tutorial002 import app
-
-client = TestClient(app)
+from tests.utils import needs_py39, needs_py310
-def test_get_item():
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial002"),
+ pytest.param("tutorial002_py39", marks=needs_py39),
+ pytest.param("tutorial002_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
+
+ client = TestClient(mod.app)
+ client.headers.clear()
+ return client
+
+
+def test_get_item(client: TestClient):
response = client.get("/items/next")
assert response.status_code == 200
assert response.json() == {
@@ -18,7 +35,7 @@ def test_get_item():
}
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py
index e1fa45201..5728d2b6b 100644
--- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py
+++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py
@@ -1,13 +1,28 @@
+import importlib
+
+import pytest
from fastapi.testclient import TestClient
-from docs_src.dataclasses.tutorial003 import app
-
-from ...utils import needs_pydanticv1, needs_pydanticv2
-
-client = TestClient(app)
+from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2
-def test_post_authors_item():
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial003"),
+ pytest.param("tutorial003_py39", marks=needs_py39),
+ pytest.param("tutorial003_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
+
+ client = TestClient(mod.app)
+ client.headers.clear()
+ return client
+
+
+def test_post_authors_item(client: TestClient):
response = client.post(
"/authors/foo/items/",
json=[{"name": "Bar"}, {"name": "Baz", "description": "Drop the Baz"}],
@@ -22,7 +37,7 @@ def test_post_authors_item():
}
-def test_get_authors():
+def test_get_authors(client: TestClient):
response = client.get("/authors/")
assert response.status_code == 200
assert response.json() == [
@@ -54,7 +69,7 @@ def test_get_authors():
@needs_pydanticv2
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
@@ -191,7 +206,7 @@ def test_openapi_schema():
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
-def test_openapi_schema_pv1():
+def test_openapi_schema_pv1(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == {
diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py
index 217159a59..c04bf3724 100644
--- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py
+++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py
@@ -8,18 +8,8 @@ client = TestClient(app)
def test_get_validation_error():
response = client.get("/items/foo")
assert response.status_code == 400, response.text
- # TODO: remove when deprecating Pydantic v1
- assert (
- # TODO: remove when deprecating Pydantic v1
- "path -> item_id" in response.text
- or "'loc': ('path', 'item_id')" in response.text
- )
- assert (
- # TODO: remove when deprecating Pydantic v1
- "value is not a valid integer" in response.text
- or "Input should be a valid integer, unable to parse string as an integer"
- in response.text
- )
+ assert "Validation errors:" in response.text
+ assert "Field: ('path', 'item_id')" in response.text
def test_get_http_error():
diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py
index 73af420ae..2df2b9889 100644
--- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py
+++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py
@@ -1,12 +1,33 @@
+import importlib
+from types import ModuleType
+
+import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient
-from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification
-
-client = TestClient(app)
+from tests.utils import needs_py310
-def test_get():
+@pytest.fixture(
+ name="mod",
+ params=[
+ pytest.param("tutorial001"),
+ pytest.param("tutorial001_py310", marks=needs_py310),
+ ],
+)
+def get_mod(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.openapi_callbacks.{request.param}")
+ return mod
+
+
+@pytest.fixture(name="client")
+def get_client(mod: ModuleType):
+ client = TestClient(mod.app)
+ client.headers.clear()
+ return client
+
+
+def test_get(client: TestClient):
response = client.post(
"/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3}
)
@@ -14,12 +35,12 @@ def test_get():
assert response.json() == {"msg": "Invoice received"}
-def test_dummy_callback():
+def test_dummy_callback(mod: ModuleType):
# Just for coverage
- invoice_notification({})
+ mod.invoice_notification({})
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py
index 4f69e4646..da5782d18 100644
--- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py
+++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py
@@ -1,13 +1,30 @@
+import importlib
+
+import pytest
from fastapi.testclient import TestClient
-from docs_src.path_operation_advanced_configuration.tutorial004 import app
-
-from ...utils import needs_pydanticv1, needs_pydanticv2
-
-client = TestClient(app)
+from ...utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2
-def test_query_params_str_validations():
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial004"),
+ pytest.param("tutorial004_py39", marks=needs_py39),
+ pytest.param("tutorial004_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(
+ f"docs_src.path_operation_advanced_configuration.{request.param}"
+ )
+
+ client = TestClient(mod.app)
+ client.headers.clear()
+ return client
+
+
+def test_query_params_str_validations(client: TestClient):
response = client.post("/items/", json={"name": "Foo", "price": 42})
assert response.status_code == 200, response.text
assert response.json() == {
@@ -20,7 +37,7 @@ def test_query_params_str_validations():
@needs_pydanticv2
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
@@ -123,7 +140,7 @@ def test_openapi_schema():
# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
-def test_openapi_schema_pv1():
+def test_openapi_schema_pv1(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py
index 8240b60a6..a90337a63 100644
--- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py
+++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py
@@ -1,14 +1,24 @@
+import importlib
+
import pytest
from fastapi.testclient import TestClient
-from ...utils import needs_pydanticv2
+from ...utils import needs_py39, needs_pydanticv2
-@pytest.fixture(name="client")
-def get_client():
- from docs_src.path_operation_advanced_configuration.tutorial007 import app
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial007"),
+ pytest.param("tutorial007_py39", marks=needs_py39),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(
+ f"docs_src.path_operation_advanced_configuration.{request.param}"
+ )
- client = TestClient(app)
+ client = TestClient(mod.app)
return client
diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py
index ef012f8a6..b38e4947c 100644
--- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py
+++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py
@@ -1,14 +1,24 @@
+import importlib
+
import pytest
from fastapi.testclient import TestClient
-from ...utils import needs_pydanticv1
+from ...utils import needs_py39, needs_pydanticv1
-@pytest.fixture(name="client")
-def get_client():
- from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial007_pv1"),
+ pytest.param("tutorial007_pv1_py39", marks=needs_py39),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(
+ f"docs_src.path_operation_advanced_configuration.{request.param}"
+ )
- client = TestClient(app)
+ client = TestClient(mod.app)
return client
diff --git a/tests/test_tutorial/test_response_directly/__init__.py b/tests/test_tutorial/test_response_directly/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_tutorial/test_response_directly/test_tutorial001.py b/tests/test_tutorial/test_response_directly/test_tutorial001.py
new file mode 100644
index 000000000..2cc4f3b0c
--- /dev/null
+++ b/tests/test_tutorial/test_response_directly/test_tutorial001.py
@@ -0,0 +1,288 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2
+
+
+@pytest.fixture(
+ name="client",
+ params=[
+ pytest.param("tutorial001"),
+ pytest.param("tutorial001_py310", marks=needs_py310),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.response_directly.{request.param}")
+
+ client = TestClient(mod.app)
+ return client
+
+
+def test_path_operation(client: TestClient):
+ response = client.put(
+ "/items/1",
+ json={
+ "title": "Foo",
+ "timestamp": "2023-01-01T12:00:00",
+ "description": "A test item",
+ },
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "description": "A test item",
+ "timestamp": "2023-01-01T12:00:00",
+ "title": "Foo",
+ }
+
+
+@needs_pydanticv2
+def test_openapi_schema_pv2(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "info": {
+ "title": "FastAPI",
+ "version": "0.1.0",
+ },
+ "openapi": "3.1.0",
+ "paths": {
+ "/items/{id}": {
+ "put": {
+ "operationId": "update_item_items__id__put",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": True,
+ "schema": {"title": "Id", "type": "string"},
+ },
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Item",
+ },
+ },
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {"schema": {}},
+ },
+ "description": "Successful Response",
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError",
+ },
+ },
+ },
+ "description": "Validation Error",
+ },
+ },
+ "summary": "Update Item",
+ },
+ },
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError",
+ },
+ "title": "Detail",
+ "type": "array",
+ },
+ },
+ "title": "HTTPValidationError",
+ "type": "object",
+ },
+ "Item": {
+ "properties": {
+ "description": {
+ "anyOf": [
+ {"type": "string"},
+ {"type": "null"},
+ ],
+ "title": "Description",
+ },
+ "timestamp": {
+ "format": "date-time",
+ "title": "Timestamp",
+ "type": "string",
+ },
+ "title": {"title": "Title", "type": "string"},
+ },
+ "required": [
+ "title",
+ "timestamp",
+ ],
+ "title": "Item",
+ "type": "object",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {"type": "string"},
+ {"type": "integer"},
+ ],
+ },
+ "title": "Location",
+ "type": "array",
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ "type": "object",
+ },
+ },
+ },
+ }
+
+
+@needs_pydanticv1
+def test_openapi_schema_pv1(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "info": {
+ "title": "FastAPI",
+ "version": "0.1.0",
+ },
+ "openapi": "3.1.0",
+ "paths": {
+ "/items/{id}": {
+ "put": {
+ "operationId": "update_item_items__id__put",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": True,
+ "schema": {
+ "title": "Id",
+ "type": "string",
+ },
+ },
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Item",
+ },
+ },
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {},
+ },
+ },
+ "description": "Successful Response",
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError",
+ },
+ },
+ },
+ "description": "Validation Error",
+ },
+ },
+ "summary": "Update Item",
+ },
+ },
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError",
+ },
+ "title": "Detail",
+ "type": "array",
+ },
+ },
+ "title": "HTTPValidationError",
+ "type": "object",
+ },
+ "Item": {
+ "properties": {
+ "description": {
+ "title": "Description",
+ "type": "string",
+ },
+ "timestamp": {
+ "format": "date-time",
+ "title": "Timestamp",
+ "type": "string",
+ },
+ "title": {
+ "title": "Title",
+ "type": "string",
+ },
+ },
+ "required": [
+ "title",
+ "timestamp",
+ ],
+ "title": "Item",
+ "type": "object",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string",
+ },
+ {
+ "type": "integer",
+ },
+ ],
+ },
+ "title": "Location",
+ "type": "array",
+ },
+ "msg": {
+ "title": "Message",
+ "type": "string",
+ },
+ "type": {
+ "title": "Error Type",
+ "type": "string",
+ },
+ },
+ "required": [
+ "loc",
+ "msg",
+ "type",
+ ],
+ "title": "ValidationError",
+ "type": "object",
+ },
+ },
+ },
+ }
diff --git a/tests/test_tutorial/test_settings/test_app02.py b/tests/test_tutorial/test_settings/test_app02.py
index eced88c04..5e1232ea0 100644
--- a/tests/test_tutorial/test_settings/test_app02.py
+++ b/tests/test_tutorial/test_settings/test_app02.py
@@ -1,20 +1,45 @@
+import importlib
+from types import ModuleType
+
+import pytest
from pytest import MonkeyPatch
-from ...utils import needs_pydanticv2
+from ...utils import needs_py39, needs_pydanticv2
+
+
+@pytest.fixture(
+ name="mod_path",
+ params=[
+ pytest.param("app02"),
+ pytest.param("app02_an"),
+ pytest.param("app02_an_py39", marks=needs_py39),
+ ],
+)
+def get_mod_path(request: pytest.FixtureRequest):
+ mod_path = f"docs_src.settings.{request.param}"
+ return mod_path
+
+
+@pytest.fixture(name="main_mod")
+def get_main_mod(mod_path: str) -> ModuleType:
+ main_mod = importlib.import_module(f"{mod_path}.main")
+ return main_mod
+
+
+@pytest.fixture(name="test_main_mod")
+def get_test_main_mod(mod_path: str) -> ModuleType:
+ test_main_mod = importlib.import_module(f"{mod_path}.test_main")
+ return test_main_mod
@needs_pydanticv2
-def test_settings(monkeypatch: MonkeyPatch):
- from docs_src.settings.app02 import main
-
+def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch):
monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
- settings = main.get_settings()
+ settings = main_mod.get_settings()
assert settings.app_name == "Awesome API"
assert settings.items_per_user == 50
@needs_pydanticv2
-def test_override_settings():
- from docs_src.settings.app02 import test_main
-
- test_main.test_app()
+def test_override_settings(test_main_mod: ModuleType):
+ test_main_mod.test_app()
diff --git a/tests/test_tutorial/test_settings/test_app03.py b/tests/test_tutorial/test_settings/test_app03.py
new file mode 100644
index 000000000..d9872c15f
--- /dev/null
+++ b/tests/test_tutorial/test_settings/test_app03.py
@@ -0,0 +1,59 @@
+import importlib
+from types import ModuleType
+
+import pytest
+from fastapi.testclient import TestClient
+from pytest import MonkeyPatch
+
+from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2
+
+
+@pytest.fixture(
+ name="mod_path",
+ params=[
+ pytest.param("app03"),
+ pytest.param("app03_an"),
+ pytest.param("app03_an_py39", marks=needs_py39),
+ ],
+)
+def get_mod_path(request: pytest.FixtureRequest):
+ mod_path = f"docs_src.settings.{request.param}"
+ return mod_path
+
+
+@pytest.fixture(name="main_mod")
+def get_main_mod(mod_path: str) -> ModuleType:
+ main_mod = importlib.import_module(f"{mod_path}.main")
+ return main_mod
+
+
+@needs_pydanticv2
+def test_settings(main_mod: ModuleType, monkeypatch: MonkeyPatch):
+ monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+ settings = main_mod.get_settings()
+ assert settings.app_name == "Awesome API"
+ assert settings.admin_email == "admin@example.com"
+ assert settings.items_per_user == 50
+
+
+@needs_pydanticv1
+def test_settings_pv1(mod_path: str, monkeypatch: MonkeyPatch):
+ monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+ config_mod = importlib.import_module(f"{mod_path}.config_pv1")
+ settings = config_mod.Settings()
+ assert settings.app_name == "Awesome API"
+ assert settings.admin_email == "admin@example.com"
+ assert settings.items_per_user == 50
+
+
+@needs_pydanticv2
+def test_endpoint(main_mod: ModuleType, monkeypatch: MonkeyPatch):
+ monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+ client = TestClient(main_mod.app)
+ response = client.get("/info")
+ assert response.status_code == 200
+ assert response.json() == {
+ "app_name": "Awesome API",
+ "admin_email": "admin@example.com",
+ "items_per_user": 50,
+ }
diff --git a/tests/test_tutorial/test_using_request_directly/__init__.py b/tests/test_tutorial/test_using_request_directly/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_tutorial/test_using_request_directly/test_tutorial001.py b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py
new file mode 100644
index 000000000..54c53ae1e
--- /dev/null
+++ b/tests/test_tutorial/test_using_request_directly/test_tutorial001.py
@@ -0,0 +1,112 @@
+from fastapi.testclient import TestClient
+
+from docs_src.using_request_directly.tutorial001 import app
+
+client = TestClient(app)
+
+
+def test_path_operation():
+ response = client.get("/items/foo")
+ assert response.status_code == 200
+ assert response.json() == {"client_host": "testclient", "item_id": "foo"}
+
+
+def test_openapi():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200
+ assert response.json() == {
+ "info": {
+ "title": "FastAPI",
+ "version": "0.1.0",
+ },
+ "openapi": "3.1.0",
+ "paths": {
+ "/items/{item_id}": {
+ "get": {
+ "operationId": "read_root_items__item_id__get",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "item_id",
+ "required": True,
+ "schema": {
+ "title": "Item Id",
+ "type": "string",
+ },
+ },
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {},
+ },
+ },
+ "description": "Successful Response",
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError",
+ },
+ },
+ },
+ "description": "Validation Error",
+ },
+ },
+ "summary": "Read Root",
+ },
+ },
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError",
+ },
+ "title": "Detail",
+ "type": "array",
+ },
+ },
+ "title": "HTTPValidationError",
+ "type": "object",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string",
+ },
+ {
+ "type": "integer",
+ },
+ ],
+ },
+ "title": "Location",
+ "type": "array",
+ },
+ "msg": {
+ "title": "Message",
+ "type": "string",
+ },
+ "type": {
+ "title": "Error Type",
+ "type": "string",
+ },
+ },
+ "required": [
+ "loc",
+ "msg",
+ "type",
+ ],
+ "title": "ValidationError",
+ "type": "object",
+ },
+ },
+ },
+ }
diff --git a/tests/test_tutorial/test_websockets/test_tutorial003.py b/tests/test_tutorial/test_websockets/test_tutorial003.py
index dbcad3b02..85efc1859 100644
--- a/tests/test_tutorial/test_websockets/test_tutorial003.py
+++ b/tests/test_tutorial/test_websockets/test_tutorial003.py
@@ -1,16 +1,45 @@
+import importlib
+from types import ModuleType
+
+import pytest
from fastapi.testclient import TestClient
-from docs_src.websockets.tutorial003 import app, html
-
-client = TestClient(app)
+from ...utils import needs_py39
-def test_get():
+@pytest.fixture(
+ name="mod",
+ params=[
+ pytest.param("tutorial003"),
+ pytest.param("tutorial003_py39", marks=needs_py39),
+ ],
+)
+def get_mod(request: pytest.FixtureRequest):
+ mod = importlib.import_module(f"docs_src.websockets.{request.param}")
+
+ return mod
+
+
+@pytest.fixture(name="html")
+def get_html(mod: ModuleType):
+ return mod.html
+
+
+@pytest.fixture(name="client")
+def get_client(mod: ModuleType):
+ client = TestClient(mod.app)
+
+ return client
+
+
+@needs_py39
+def test_get(client: TestClient, html: str):
response = client.get("/")
assert response.text == html
-def test_websocket_handle_disconnection():
+@needs_py39
+def test_websocket_handle_disconnection(client: TestClient):
with client.websocket_connect("/ws/1234") as connection, client.websocket_connect(
"/ws/5678"
) as connection_two:
diff --git a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py
deleted file mode 100644
index 06c4a9279..000000000
--- a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import pytest
-from fastapi import FastAPI
-from fastapi.testclient import TestClient
-
-from ...utils import needs_py39
-
-
-@pytest.fixture(name="app")
-def get_app():
- from docs_src.websockets.tutorial003_py39 import app
-
- return app
-
-
-@pytest.fixture(name="html")
-def get_html():
- from docs_src.websockets.tutorial003_py39 import html
-
- return html
-
-
-@pytest.fixture(name="client")
-def get_client(app: FastAPI):
- client = TestClient(app)
-
- return client
-
-
-@needs_py39
-def test_get(client: TestClient, html: str):
- response = client.get("/")
- assert response.text == html
-
-
-@needs_py39
-def test_websocket_handle_disconnection(client: TestClient):
- with client.websocket_connect("/ws/1234") as connection, client.websocket_connect(
- "/ws/5678"
- ) as connection_two:
- connection.send_text("Hello from 1234")
- data1 = connection.receive_text()
- assert data1 == "You wrote: Hello from 1234"
- data2 = connection_two.receive_text()
- client1_says = "Client #1234 says: Hello from 1234"
- assert data2 == client1_says
- data1 = connection.receive_text()
- assert data1 == client1_says
- connection_two.close()
- data1 = connection.receive_text()
- assert data1 == "Client #5678 left the chat"