Merge branch 'master' into fix_openapi_for_response_model_with_nested_computed_field

This commit is contained in:
Motov Yurii 2025-12-12 17:47:16 +01:00 committed by GitHub
commit 20527a7148
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 10667 additions and 233 deletions

View File

@ -44,22 +44,45 @@ jobs:
run: bash scripts/lint.sh run: bash scripts/lint.sh
test: test:
runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: os: [ windows-latest, macos-latest ]
- "3.14" python-version: [ "3.14" ]
- "3.13" pydantic-version: [ "pydantic-v2" ]
- "3.12" include:
- "3.11" - os: macos-latest
- "3.10" python-version: "3.8"
- "3.9"
- "3.8"
pydantic-version: ["pydantic-v1", "pydantic-v2"]
exclude:
- python-version: "3.14"
pydantic-version: "pydantic-v1" pydantic-version: "pydantic-v1"
- os: windows-latest
python-version: "3.8"
pydantic-version: "pydantic-v2"
coverage: coverage
- os: ubuntu-latest
python-version: "3.9"
pydantic-version: "pydantic-v1"
coverage: coverage
- os: macos-latest
python-version: "3.10"
pydantic-version: "pydantic-v2"
- os: windows-latest
python-version: "3.11"
pydantic-version: "pydantic-v1"
- os: ubuntu-latest
python-version: "3.12"
pydantic-version: "pydantic-v2"
- os: macos-latest
python-version: "3.13"
pydantic-version: "pydantic-v1"
- os: windows-latest
python-version: "3.13"
pydantic-version: "pydantic-v2"
coverage: coverage
- os: ubuntu-latest
python-version: "3.14"
pydantic-version: "pydantic-v2"
coverage: coverage
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }}
steps: steps:
- name: Dump GitHub context - name: Dump GitHub context
env: env:
@ -96,10 +119,12 @@ jobs:
env: env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
- name: Store coverage files - name: Store coverage files
if: matrix.coverage == 'coverage'
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
path: coverage path: coverage
include-hidden-files: true include-hidden-files: true

View File

@ -15,7 +15,7 @@ Use as follows:
The tests: The tests:
## Code snippets { #code-snippets} ## Code snippets { #code-snippets }
//// tab | Test //// tab | Test
@ -53,7 +53,7 @@ See for example section `### Quotes` in `docs/de/llm-prompt.md`.
//// ////
## Quotes in code snippets { #quotes-in-code-snippets} ## Quotes in code snippets { #quotes-in-code-snippets }
//// tab | Test //// tab | Test

View File

@ -52,13 +52,13 @@ The key features are:
<!-- sponsors --> <!-- sponsors -->
### Keystone Sponsor ### Keystone Sponsor { #keystone-sponsor }
{% for sponsor in sponsors.keystone -%} {% for sponsor in sponsors.keystone -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor -%} {% endfor -%}
### Gold and Silver Sponsors ### Gold and Silver Sponsors { #gold-and-silver-sponsors }
{% for sponsor in sponsors.gold -%} {% for sponsor in sponsors.gold -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>

View File

@ -7,15 +7,38 @@ hide:
## Latest Changes ## Latest Changes
## 0.124.4
### Fixes
* 🐛 Fix parameter aliases. PR [#14371](https://github.com/fastapi/fastapi/pull/14371) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.124.3
### Fixes
* 🐛 Fix support for tagged union with discriminator inside of `Annotated` with `Body()`. PR [#14512](https://github.com/fastapi/fastapi/pull/14512) by [@tiangolo](https://github.com/tiangolo).
### Refactors
* ✅ Add set of tests for request parameters and alias. PR [#14358](https://github.com/fastapi/fastapi/pull/14358) by [@YuriiMotov](https://github.com/YuriiMotov).
### Docs ### Docs
* 📝 Tweak links format. PR [#14505](https://github.com/fastapi/fastapi/pull/14505) by [@tiangolo](https://github.com/tiangolo).
* 📝 Update docs about re-raising validation errors, do not include string as is to not leak information. PR [#14487](https://github.com/fastapi/fastapi/pull/14487) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs about re-raising validation errors, do not include string as is to not leak information. PR [#14487](https://github.com/fastapi/fastapi/pull/14487) by [@tiangolo](https://github.com/tiangolo).
* 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo).
### Translations ### Translations
* 🌐 Sync Russian docs. PR [#14509](https://github.com/fastapi/fastapi/pull/14509) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🌐 Sync German docs. PR [#14488](https://github.com/fastapi/fastapi/pull/14488) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Sync German docs. PR [#14488](https://github.com/fastapi/fastapi/pull/14488) by [@nilslindemann](https://github.com/nilslindemann).
### Internal
* 👷 Tweak coverage to not pass Smokeshow max file size limit. PR [#14507](https://github.com/fastapi/fastapi/pull/14507) by [@tiangolo](https://github.com/tiangolo).
* ✅ Expand test matrix to include Windows and MacOS. PR [#14171](https://github.com/fastapi/fastapi/pull/14171) by [@svlandeg](https://github.com/svlandeg).
## 0.124.2 ## 0.124.2
### Fixes ### Fixes

View File

@ -15,7 +15,7 @@
Тесты: Тесты:
## Фрагменты кода { #code-snippets} ## Фрагменты кода { #code-snippets }
//// tab | Тест //// tab | Тест
@ -53,7 +53,7 @@ LLM, вероятно, переведёт это неправильно. Инт
//// ////
## Кавычки во фрагментах кода { #quotes-in-code-snippets} ## Кавычки во фрагментах кода { #quotes-in-code-snippets }
//// tab | Тест //// tab | Тест

View File

@ -175,7 +175,7 @@
Например, вы можете добавить дополнительный тип содержимого `image/png`, объявив, что ваша операция пути может возвращать JSONобъект (с типом содержимого `application/json`) или PNGизображение: Например, вы можете добавить дополнительный тип содержимого `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 | Примечание /// 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 } ## Дополнительная информация об ответах OpenAPI { #more-information-about-openapi-responses }

View File

@ -144,7 +144,7 @@ checker(q="somequery")
### Фоновые задачи и зависимости с `yield`, технические детали { #background-tasks-and-dependencies-with-yield-technical-details } ### Фоновые задачи и зависимости с `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` выполнялся после завершения фоновых задач. Так было сделано в основном для того, чтобы можно было использовать те же объекты, «отданные» зависимостями через `yield`, внутри фоновых задач, потому что код после `yield` выполнялся после завершения фоновых задач.

View File

@ -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 } ### Отключить автоматическое добавление сервера из `root_path` { #disable-automatic-server-from-root-path }
Если вы не хотите, чтобы FastAPI добавлял автоматический сервер, используя `root_path`, укажите параметр `root_path_in_servers=False`: Если вы не хотите, чтобы FastAPI добавлял автоматический сервер, используя `root_path`, укажите параметр `root_path_in_servers=False`:

View File

@ -4,7 +4,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
Но FastAPI также поддерживает использование <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> тем же способом: Но FastAPI также поддерживает использование <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> тем же способом:
{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *} {* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">встроенная поддержка `dataclasses`</a>. Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">встроенная поддержка `dataclasses`</a>.
@ -32,7 +32,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
Вы также можете использовать `dataclasses` в параметре `response_model`: Вы также можете использовать `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. Этот dataclass будет автоматически преобразован в Pydantic dataclass.
@ -48,7 +48,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
В таком случае вы можете просто заменить стандартные `dataclasses` на `pydantic.dataclasses`, которая является полностью совместимой заменой (drop-in replacement): В таком случае вы можете просто заменить стандартные `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`. 1. Мы по-прежнему импортируем `field` из стандартных `dataclasses`.

View File

@ -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 | Совет /// tip | Совет
@ -90,7 +90,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
Сначала создайте новый `APIRouter`, который будет содержать один или несколько обратных вызовов. Сначала создайте новый `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 } ### Создайте *операцию пути* для обратного вызова { #create-the-callback-path-operation }
@ -101,7 +101,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
* Вероятно, в ней должно быть объявление тела запроса, например `body: InvoiceEvent`. * Вероятно, в ней должно быть объявление тела запроса, например `body: InvoiceEvent`.
* А также может быть объявление модели ответа, например `response_model=InvoiceEventReceived`. * А также может быть объявление модели ответа, например `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 основных отличия от обычной *операции пути*: Есть 2 основных отличия от обычной *операции пути*:
@ -169,7 +169,7 @@ https://www.external.org/events/invoices/2expen51ve
Теперь используйте параметр `callbacks` в *декораторе операции пути вашего API*, чтобы передать атрибут `.routes` (это, по сути, просто `list` маршрутов/*операций пути*) из этого маршрутизатора обратных вызовов: Теперь используйте параметр `callbacks` в *декораторе операции пути вашего API*, чтобы передать атрибут `.routes` (это, по сути, просто `list` маршрутов/*операций пути*) из этого маршрутизатора обратных вызовов:
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *} {* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *}
/// tip | Совет /// tip | Совет

View File

@ -50,7 +50,7 @@
Эта часть не попадёт в документацию, но другие инструменты (например, Sphinx) смогут использовать остальное. Эта часть не попадёт в документацию, но другие инструменты (например, 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 } ## Дополнительные ответы { #additional-responses }
@ -155,13 +155,13 @@
//// tab | Pydantic v2 //// 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 //// 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 //// 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 //// 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] *}
//// ////

View File

@ -34,7 +34,7 @@
В таких случаях вы можете использовать `jsonable_encoder` для преобразования данных перед передачей их в ответ: В таких случаях вы можете использовать `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 | Технические детали /// note | Технические детали

View File

@ -148,7 +148,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p
Продолжая предыдущий пример, ваш файл `config.py` может выглядеть так: Продолжая предыдущий пример, ваш файл `config.py` может выглядеть так:
{* ../../docs_src/settings/app02/config.py hl[10] *} {* ../../docs_src/settings/app02_an_py39/config.py hl[10] *}
Обратите внимание, что теперь мы не создаем экземпляр по умолчанию `settings = Settings()`. Обратите внимание, что теперь мы не создаем экземпляр по умолчанию `settings = Settings()`.
@ -174,7 +174,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p
Далее будет очень просто предоставить другой объект настроек во время тестирования, создав переопределение зависимости для `get_settings`: Далее будет очень просто предоставить другой объект настроек во время тестирования, создав переопределение зависимости для `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`, а затем возвращаем этот новый объект. В переопределении зависимости мы задаем новое значение `admin_email` при создании нового объекта `Settings`, а затем возвращаем этот новый объект.
@ -217,7 +217,7 @@ APP_NAME="ChimichangApp"
//// tab | Pydantic v2 //// tab | Pydantic v2
{* ../../docs_src/settings/app03_an/config.py hl[9] *} {* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
/// tip | Совет /// tip | Совет
@ -229,7 +229,7 @@ APP_NAME="ChimichangApp"
//// tab | Pydantic v1 //// 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 | Совет /// tip | Совет

View File

@ -4,11 +4,19 @@
В большинстве случаев у основных облачных провайдеров есть руководства по развертыванию FastAPI на их платформе. В большинстве случаев у основных облачных провайдеров есть руководства по развертыванию FastAPI на их платформе.
## FastAPI Cloud { #fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** создан тем же автором и командой, стоящими за **FastAPI**.
Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
Он переносит тот же **опыт разработчика** создания приложений с FastAPI на их **развертывание** в облаке. 🎉
FastAPI Cloud — основной спонсор и источник финансирования для open source проектов *FastAPI and friends*. ✨
## Облачные провайдеры — спонсоры { #cloud-providers-sponsors } ## Облачные провайдеры — спонсоры { #cloud-providers-sponsors }
Некоторые облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ — это обеспечивает непрерывное и здоровое развитие FastAPI и его экосистемы. Некоторые другие облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ тоже. 🙇
И это показывает их искреннюю приверженность FastAPI и его сообществу (вам): они не только хотят предоставить вам хороший сервис, но и стремятся гарантировать, что у вас будет хороший и стабильный фреймворк — FastAPI. 🙇
Возможно, вы захотите попробовать их сервисы и воспользоваться их руководствами: Возможно, вы захотите попробовать их сервисы и воспользоваться их руководствами:

View File

@ -0,0 +1,65 @@
# FastAPI Cloud { #fastapi-cloud }
Вы можете развернуть своё приложение FastAPI в <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a> одной командой, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀
## Вход { #login }
Убедитесь, что у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉).
Затем выполните вход:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
## Деплой { #deploy }
Теперь разверните приложение одной командой:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
Вот и всё! Теперь вы можете открыть своё приложение по этому URL. ✨
## О FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** создан тем же автором и командой, что и **FastAPI**.
Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
Он переносит тот же **опыт разработчика**, что вы получаете при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉
Он также возьмёт на себя большинство вещей, которые требуются при развертывании приложения, например:
* HTTPS
* Репликация с автоматическим масштабированием на основе запросов
* и т.д.
FastAPI Cloud — основной спонсор и источник финансирования open sourceпроектов «FastAPI и друзья». ✨
## Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
FastAPI — проект с открытым исходным кодом и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор.
Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓
## Развертывание на собственном сервере { #deploy-your-own-server }
Позже в этом руководстве по **развертыванию** я также расскажу все детали — чтобы вы понимали, что происходит, что нужно сделать и как развернуть приложения FastAPI самостоятельно, в том числе на собственных серверах. 🤓

View File

@ -12,10 +12,12 @@
## Стратегии развёртывания { #deployment-strategies } ## Стратегии развёртывания { #deployment-strategies }
В зависимости от вашего конкретного случая, есть несколько способов сделать это. Есть несколько способов сделать это, в зависимости от вашего конкретного случая и используемых вами инструментов.
Вы можете **развернуть сервер** самостоятельно, используя различные инструменты. Например, можно использовать **облачный сервис**, который выполнит часть работы за вас. Также возможны и другие варианты. Вы можете **развернуть сервер** самостоятельно, используя различные инструменты. Например, можно использовать **облачный сервис**, который выполнит часть работы за вас. Также возможны и другие варианты.
Например, мы, команда, стоящая за FastAPI, создали <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a>, чтобы сделать развёртывание приложений FastAPI в облаке как можно более простым и прямолинейным, с тем же удобством для разработчика, что и при работе с FastAPI.
В этом блоке я покажу вам некоторые из основных концепций, которые вы, вероятно, должны иметь в виду при развертывании приложения **FastAPI** (хотя большинство из них применимо к любому другому типу веб-приложений). В этом блоке я покажу вам некоторые из основных концепций, которые вы, вероятно, должны иметь в виду при развертывании приложения **FastAPI** (хотя большинство из них применимо к любому другому типу веб-приложений).
В последующих разделах вы узнаете больше деталей и методов, необходимых для этого. ✨ В последующих разделах вы узнаете больше деталей и методов, необходимых для этого. ✨

View File

@ -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, <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>.
Но если по какой-то причине ваши клиенты зависят от старого поведения, вы можете вернуть его, переопределив метод `make_not_authenticated_error` в ваших Security-классах.
Например, вы можете создать подкласс `HTTPBearer`, который будет возвращать ошибку `403 Forbidden` вместо стандартной `401 Unauthorized`:
{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
/// tip | Совет
Обратите внимание, что функция возвращает экземпляр исключения, не вызывает его. Выброс выполняется остальным внутренним кодом.
///

View File

@ -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`. Вы можете переопределить любую из них, указав другое значение в аргументе `swagger_ui_parameters`.

View File

@ -42,7 +42,7 @@
Таким образом, один и тот же класс маршрута сможет обрабатывать как gzip-сжатые, так и несжатые запросы. Таким образом, один и тот же класс маршрута сможет обрабатывать как 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 } ### Создать пользовательский класс `GzipRoute` { #create-a-custom-gziproute-class }
@ -54,7 +54,7 @@
Здесь мы используем её, чтобы создать `GzipRequest` из исходного HTTP-запроса. Здесь мы используем её, чтобы создать `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 | Технические детали /// note | Технические детали
@ -92,18 +92,18 @@
Нужно лишь обработать запрос внутри блока `try`/`except`: Нужно лишь обработать запрос внутри блока `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` всё ещё будет в области видимости, поэтому мы сможем прочитать тело запроса и использовать его при обработке ошибки: Если произойдёт исключение, экземпляр `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 } ## Пользовательский класс `APIRoute` в роутере { #custom-apiroute-class-in-a-router }
Вы также можете задать параметр `route_class` у `APIRouter`: Вы также можете задать параметр `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` в ответе с временем, затраченным на формирование ответа: В этом примере *операции пути*, объявленные в `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] *}

View File

@ -52,14 +52,20 @@ FastAPI — это современный, быстрый (высокопрои
<!-- sponsors --> <!-- sponsors -->
{% if sponsors %} ### Ключевой-спонсор { #keystone-sponsor }
{% for sponsor in sponsors.keystone -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor -%}
### Золотые и серебряные спонсоры { #gold-and-silver-sponsors }
{% for sponsor in sponsors.gold -%} {% for sponsor in sponsors.gold -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor -%} {% endfor -%}
{%- for sponsor in sponsors.silver -%} {%- for sponsor in sponsors.silver -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor %} {% endfor %}
{% endif %}
<!-- /sponsors --> <!-- /sponsors -->
@ -444,6 +450,58 @@ item: Item
* **сессии с использованием cookie** * **сессии с использованием cookie**
* ...и многое другое. * ...и многое другое.
### Разверните приложение (опционально) { #deploy-your-app-optional }
При желании вы можете развернуть своё приложение FastAPI в <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀
Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть ваше приложение одной командой.
Перед развертыванием убедитесь, что вы вошли в систему:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
Затем разверните приложение:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
Вот и всё! Теперь вы можете открыть ваше приложение по этой ссылке. ✨
#### О FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** создан тем же автором и командой, что и **FastAPI**.
Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API при минимальных усилиях.
Он переносит тот же **опыт разработчика**, что и при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉
FastAPI Cloud — основной спонсор и источник финансирования для проектов с открытым исходным кодом из экосистемы *FastAPI and friends*. ✨
#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
FastAPI — это open source и стандартизированный фреймворк. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор.
Следуйте руководствам вашего облачного провайдера по развертыванию приложений FastAPI. 🤓
## Производительность { #performance } ## Производительность { #performance }
Независимые бенчмарки TechEmpower показывают приложения **FastAPI**, работающие под управлением Uvicorn, как <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">один из самых быстрых доступных фреймворков Python</a>, уступающий только самим Starlette и Uvicorn (используются внутри FastAPI). (*) Независимые бенчмарки TechEmpower показывают приложения **FastAPI**, работающие под управлением Uvicorn, как <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">один из самых быстрых доступных фреймворков Python</a>, уступающий только самим Starlette и Uvicorn (используются внутри FastAPI). (*)

View File

@ -13,8 +13,8 @@
- 🔍 [Pydantic](https://docs.pydantic.dev), используется FastAPI, для валидации данных и управления настройками. - 🔍 [Pydantic](https://docs.pydantic.dev), используется FastAPI, для валидации данных и управления настройками.
- 💾 [PostgreSQL](https://www.postgresql.org) в качестве SQLбазы данных. - 💾 [PostgreSQL](https://www.postgresql.org) в качестве SQLбазы данных.
- 🚀 [React](https://react.dev) для фронтенда. - 🚀 [React](https://react.dev) для фронтенда.
- 💃 Используются TypeScript, хуки, [Vite](https://vitejs.dev) и другие части современного фронтенд‑стека. - 💃 Используются TypeScript, хуки, Vite и другие части современного фронтенд‑стека.
- 🎨 [Chakra UI](https://chakra-ui.com) для компонентов фронтенда. - 🎨 [Tailwind CSS](https://tailwindcss.com) и [shadcn/ui](https://ui.shadcn.com) для компонентов фронтенда.
- 🤖 Автоматически сгенерированный фронтенд‑клиент. - 🤖 Автоматически сгенерированный фронтенд‑клиент.
- 🧪 [Playwright](https://playwright.dev) для EndtoEnd тестирования. - 🧪 [Playwright](https://playwright.dev) для EndtoEnd тестирования.
- 🦇 Поддержка тёмной темы. - 🦇 Поддержка тёмной темы.

View File

@ -1,3 +1,3 @@
# Ресурсы { #resources } # Ресурсы { #resources }
Дополнительные ресурсы, внешние ссылки, статьи и многое другое. ✈️ Дополнительные ресурсы, внешние ссылки и многое другое. ✈️

View File

@ -85,17 +85,13 @@ from app.routers import items
Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`. Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`.
```Python hl_lines="1 3" title="app/routers/users.py" {* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *}
{!../../docs_src/bigger_applications/app/routers/users.py!}
```
### Создание *эндпоинтов* с помощью `APIRouter` { #path-operations-with-apirouter } ### Создание *эндпоинтов* с помощью `APIRouter` { #path-operations-with-apirouter }
В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`: В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`:
```Python hl_lines="6 11 16" title="app/routers/users.py" {* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}
{!../../docs_src/bigger_applications/app/routers/users.py!}
```
Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`. Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`.
@ -119,35 +115,7 @@ from app.routers import items
Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка: Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка:
//// tab | Python 3.9+ {* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}
```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!}
```
////
/// tip | Подсказка /// tip | Подсказка
@ -180,9 +148,7 @@ from app.routers import items
Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*, Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*,
мы добавим их в `APIRouter`. мы добавим их в `APIRouter`.
```Python hl_lines="5-10 16 21" title="app/routers/items.py" {* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}
{!../../docs_src/bigger_applications/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_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *}
{!../../docs_src/bigger_applications/app/routers/items.py!}
```
#### Как работает относительный импорт? { #how-relative-imports-work } #### Как работает относительный импорт? { #how-relative-imports-work }
@ -313,9 +277,7 @@ from ...dependencies import get_token_header
Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*: Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*:
```Python hl_lines="30-31" title="app/routers/items.py" {* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *}
{!../../docs_src/bigger_applications/app/routers/items.py!}
```
/// tip | Подсказка /// tip | Подсказка
@ -341,17 +303,13 @@ from ...dependencies import get_token_header
Мы даже можем объявить [глобальные зависимости](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора: Мы даже можем объявить [глобальные зависимости](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора:
```Python hl_lines="1 3 7" title="app/main.py" {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *}
{!../../docs_src/bigger_applications/app/main.py!}
```
### Импорт `APIRouter` { #import-the-apirouter } ### Импорт `APIRouter` { #import-the-apirouter }
Теперь мы импортируем другие суб-модули, содержащие `APIRouter`: Теперь мы импортируем другие суб-модули, содержащие `APIRouter`:
```Python hl_lines="4-5" title="app/main.py" {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *}
{!../../docs_src/bigger_applications/app/main.py!}
```
Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`. Так как файлы `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_an_py39/main.py hl[5] title["app/main.py"] *}
{!../../docs_src/bigger_applications/app/main.py!}
```
### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` { #include-the-apirouters-for-users-and-items } ### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` { #include-the-apirouters-for-users-and-items }
Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`: Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`:
```Python hl_lines="10-11" title="app/main.py" {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *}
{!../../docs_src/bigger_applications/app/main.py!}
```
/// info | Примечание /// info | Примечание
@ -465,17 +419,13 @@ from .routers.users import router
В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов, В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов,
то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`: то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`:
```Python hl_lines="3" title="app/internal/admin.py" {* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *}
{!../../docs_src/bigger_applications/app/internal/admin.py!}
```
Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`). Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`).
Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`. Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`.
```Python hl_lines="14-17" title="app/main.py" {* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *}
{!../../docs_src/bigger_applications/app/main.py!}
```
Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.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_an_py39/main.py hl[21:23] title["app/main.py"] *}
{!../../docs_src/bigger_applications/app/main.py!}
```
и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`. и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`.

View File

@ -50,7 +50,7 @@
Вы можете сконфигурировать Pydantic-модель так, чтобы запретить (`forbid`) любые дополнительные (`extra`) поля: Вы можете сконфигурировать 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**, то в ответ он получит **ошибку**. Если клиент попробует отправить **дополнительные cookies**, то в ответ он получит **ошибку**.

View File

@ -143,6 +143,42 @@ OpenAPI определяет схему API для вашего API. И эта
Вы также можете использовать её для автоматической генерации кода для клиентов, которые взаимодействуют с вашим API. Например, для фронтенд-, мобильных или IoT-приложений. Вы также можете использовать её для автоматической генерации кода для клиентов, которые взаимодействуют с вашим API. Например, для фронтенд-, мобильных или IoT-приложений.
### Разверните приложение (необязательно) { #deploy-your-app-optional }
При желании вы можете развернуть своё приложение FastAPI в <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, перейдите и присоединитесь к списку ожидания, если ещё не сделали этого. 🚀
Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть приложение одной командой.
Перед развертыванием убедитесь, что вы вошли в систему:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
Затем разверните приложение:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
Готово! Теперь вы можете открыть своё приложение по этому URL. ✨
## Рассмотрим поэтапно { #recap-step-by-step } ## Рассмотрим поэтапно { #recap-step-by-step }
### Шаг 1: импортируйте `FastAPI` { #step-1-import-fastapi } ### Шаг 1: импортируйте `FastAPI` { #step-1-import-fastapi }
@ -314,6 +350,26 @@ https://example.com/items/foo
Многие другие объекты и модели будут автоматически преобразованы в JSON (включая ORM и т. п.). Попробуйте использовать те, что вам привычнее, с высокой вероятностью они уже поддерживаются. Многие другие объекты и модели будут автоматически преобразованы в JSON (включая ORM и т. п.). Попробуйте использовать те, что вам привычнее, с высокой вероятностью они уже поддерживаются.
### Шаг 6: разверните приложение { #step-6-deploy-it }
Разверните приложение в **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** одной командой: `fastapi deploy`. 🎉
#### О FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** создан тем же автором и командой, что и **FastAPI**.
Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
Он переносит тот же **опыт разработчика** при создании приложений с FastAPI на их **развертывание** в облаке. 🎉
FastAPI Cloud — основной спонсор и источник финансирования для open-source проектов «FastAPI и друзья». ✨
#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
FastAPI — open-source и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера по вашему выбору.
Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓
## Резюме { #recap } ## Резюме { #recap }
* Импортируйте `FastAPI`. * Импортируйте `FastAPI`.
@ -321,3 +377,4 @@ https://example.com/items/foo
* Напишите **декоратор операции пути**, например `@app.get("/")`. * Напишите **декоратор операции пути**, например `@app.get("/")`.
* Определите **функцию операции пути**; например, `def root(): ...`. * Определите **функцию операции пути**; например, `def root(): ...`.
* Запустите сервер разработки командой `fastapi dev`. * Запустите сервер разработки командой `fastapi dev`.
* При желании разверните приложение командой `fastapi deploy`.

View File

@ -81,7 +81,7 @@
## Установка пользовательских обработчиков исключений { #install-custom-exception-handlers } ## Установка пользовательских обработчиков исключений { #install-custom-exception-handlers }
Вы можете добавить пользовательские обработчики исключений с помощью <a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">то же самое исключение - утилиты от Starlette</a>. Вы можете добавить пользовательские обработчики исключений с помощью <a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">тех же утилит обработки исключений из Starlette</a>.
Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`. Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`.
@ -117,7 +117,7 @@
Вы можете переопределить эти обработчики исключений на свои собственные. Вы можете переопределить эти обработчики исключений на свои собственные.
### Переопределение исключений проверки запроса { #override-request-validation-exceptions } ### Переопределение обработчика исключений проверки запроса { #override-request-validation-exceptions }
Когда запрос содержит недопустимые данные, **FastAPI** внутренне вызывает ошибку `RequestValidationError`. Когда запрос содержит недопустимые данные, **FastAPI** внутренне вызывает ошибку `RequestValidationError`.
@ -127,7 +127,7 @@
Обработчик исключения получит объект `Request` и исключение. Обработчик исключения получит объект `Request` и исключение.
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} {* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *}
Теперь, если перейти к `/items/foo`, то вместо стандартной JSON-ошибки с: Теперь, если перейти к `/items/foo`, то вместо стандартной JSON-ошибки с:
@ -149,36 +149,17 @@
вы получите текстовую версию: вы получите текстовую версию:
``` ```
1 validation error Validation errors:
path -> item_id Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
value is not a valid integer (type=type_error.integer)
``` ```
#### `RequestValidationError` или `ValidationError` { #requestvalidationerror-vs-validationerror }
/// warning | Внимание
Это технические детали, которые можно пропустить, если они не важны для вас сейчас.
///
`RequestValidationError` является подклассом Pydantic <a href="https://docs.pydantic.dev/latest/concepts/models/#error-handling" class="external-link" target="_blank">`ValidationError`</a>.
**FastAPI** использует его для того, чтобы, если вы используете Pydantic-модель в `response_model`, и ваши данные содержат ошибку, вы увидели ошибку в журнале.
Но клиент/пользователь этого не увидит. Вместо этого клиент получит сообщение "Internal Server Error" с кодом состояния HTTP `500`.
Так и должно быть, потому что если в вашем *ответе* или где-либо в вашем коде (не в *запросе* клиента) возникает Pydantic `ValidationError`, то это действительно ошибка в вашем коде.
И пока вы не устраните ошибку, ваши клиенты/пользователи не должны иметь доступа к внутренней информации о ней, так как это может привести к уязвимости в системе безопасности.
### Переопределите обработчик ошибок `HTTPException` { #override-the-httpexception-error-handler } ### Переопределите обработчик ошибок `HTTPException` { #override-the-httpexception-error-handler }
Аналогичным образом можно переопределить обработчик `HTTPException`. Аналогичным образом можно переопределить обработчик `HTTPException`.
Например, для этих ошибок можно вернуть обычный текстовый ответ вместо JSON: Например, для этих ошибок можно вернуть обычный текстовый ответ вместо 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 | Технические детали /// note | Технические детали
@ -188,6 +169,14 @@ path -> item_id
/// ///
/// warning | Внимание
Имейте в виду, что `RequestValidationError` содержит информацию об имени файла и строке, где произошла ошибка валидации, чтобы вы могли при желании отобразить её в логах с релевантными данными.
Но это означает, что если вы просто преобразуете её в строку и вернёте эту информацию напрямую, вы можете допустить небольшую утечку информации о своей системе, поэтому здесь код извлекает и показывает каждую ошибку отдельно.
///
### Используйте тело `RequestValidationError` { #use-the-requestvalidationerror-body } ### Используйте тело `RequestValidationError` { #use-the-requestvalidationerror-body }
Ошибка `RequestValidationError` содержит полученное `тело` с недопустимыми данными. Ошибка `RequestValidationError` содержит полученное `тело` с недопустимыми данными.

View File

@ -1,4 +1,4 @@
# Настройка авторизации # Настройка авторизации { #security }
Существует множество способов обеспечения безопасности, аутентификации и авторизации. Существует множество способов обеспечения безопасности, аутентификации и авторизации.
@ -10,11 +10,11 @@
Но сначала давайте рассмотрим некоторые небольшие концепции. Но сначала давайте рассмотрим некоторые небольшие концепции.
## Куда-то торопишься? ## Куда-то торопишься? { #in-a-hurry }
Если вам не нужна информация о каких-либо из следующих терминов и вам просто нужно добавить защиту с аутентификацией на основе логина и пароля *прямо сейчас*, переходите к следующим главам. Если вам не нужна информация о каких-либо из следующих терминов и вам просто нужно добавить защиту с аутентификацией на основе логина и пароля *прямо сейчас*, переходите к следующим главам.
## OAuth2 ## OAuth2 { #oauth2 }
OAuth2 - это протокол, который определяет несколько способов обработки аутентификации и авторизации. OAuth2 - это протокол, который определяет несколько способов обработки аутентификации и авторизации.
@ -24,7 +24,7 @@ OAuth2 включает в себя способы аутентификации
Это то, что используют под собой все кнопки "вход с помощью Facebook, Google, X (Twitter), GitHub" на страницах авторизации. Это то, что используют под собой все кнопки "вход с помощью Facebook, Google, X (Twitter), GitHub" на страницах авторизации.
### OAuth 1 ### OAuth 1 { #oauth-1 }
Ранее использовался протокол OAuth 1, который сильно отличается от OAuth2 и является более сложным, поскольку он включал прямые описания того, как шифровать сообщение. Ранее использовался протокол OAuth 1, который сильно отличается от OAuth2 и является более сложным, поскольку он включал прямые описания того, как шифровать сообщение.
@ -34,11 +34,11 @@ OAuth2 не указывает, как шифровать сообщение, о
/// tip | Подсказка /// 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**. OpenID Connect - это еще один протокол, основанный на **OAuth2**.
@ -48,7 +48,7 @@ OpenID Connect - это еще один протокол, основанный
Но вход в Facebook не поддерживает OpenID Connect. У него есть собственная вариация OAuth2. Но вход в Facebook не поддерживает OpenID Connect. У него есть собственная вариация OAuth2.
### OpenID (не "OpenID Connect") ### OpenID (не "OpenID Connect") { #openid-not-openid-connect }
Также ранее использовался стандарт "OpenID", который пытался решить ту же проблему, что и **OpenID Connect**, но не был основан на OAuth2. Также ранее использовался стандарт "OpenID", который пытался решить ту же проблему, что и **OpenID Connect**, но не был основан на OAuth2.
@ -56,7 +56,7 @@ OpenID Connect - это еще один протокол, основанный
В настоящее время не очень популярен и не используется. В настоящее время не очень популярен и не используется.
## OpenAPI ## OpenAPI { #openapi }
OpenAPI (ранее известный как Swagger) - это открытая спецификация для создания API (в настоящее время является частью Linux Foundation). OpenAPI (ранее известный как Swagger) - это открытая спецификация для создания API (в настоящее время является частью Linux Foundation).
@ -97,7 +97,7 @@ OpenAPI может использовать следующие схемы авт
/// ///
## Преимущества **FastAPI** ## Преимущества **FastAPI** { #fastapi-utilities }
Fast API предоставляет несколько инструментов для каждой из этих схем безопасности в модуле `fastapi.security`, которые упрощают использование этих механизмов безопасности. Fast API предоставляет несколько инструментов для каждой из этих схем безопасности в модуле `fastapi.security`, которые упрощают использование этих механизмов безопасности.

View File

@ -63,9 +63,9 @@ $ pip install sqlmodel
* `table=True` сообщает SQLModel, что это *модель-таблица*, она должна представлять **таблицу** в SQL базе данных, это не просто *модель данных* (как обычный класс Pydantic). * `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`)* в схеме базы данных. См. <a href="https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#primary-key-id" class="external-link" target="_blank">документацию SQLModel о первичных ключах</a> для подробностей.
* `Field(index=True)` сообщает SQLModel, что нужно создать **SQL индекс** для этого столбца, что позволит быстрее выполнять выборки при чтении данных, отфильтрованных по этому столбцу. * `Field(index=True)` сообщает SQLModel, что нужно создать **SQL индекс** для этого столбца, что позволит быстрее выполнять выборки при чтении данных, отфильтрованных по этому столбцу.
@ -107,7 +107,7 @@ $ pip install sqlmodel
Здесь мы создаём таблицы в обработчике события запуска приложения. Здесь мы создаём таблицы в обработчике события запуска приложения.
Для продакшна вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓 Для продакшн вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓
/// tip | Подсказка /// tip | Подсказка

View File

@ -121,63 +121,13 @@ $ pip install httpx
Обе *операции пути* требуют наличия в запросе заголовка `X-Token`. Обе *операции пути* требуют наличия в запросе заголовка `X-Token`.
//// tab | Python 3.10+ {* ../../docs_src/app_testing/app_b_an_py310/main.py *}
```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!}
```
////
### Расширенный файл тестов { #extended-testing-file } ### Расширенный файл тестов { #extended-testing-file }
Теперь обновим файл `test_main.py`, добавив в него тестов: Теперь обновим файл `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. Если Вы не знаете, как передать информацию в запросе, можете воспользоваться поисковиком (погуглить) и задать вопрос: "Как передать информацию в запросе с помощью `httpx`", можно даже спросить: "Как передать информацию в запросе с помощью `requests`", поскольку дизайн HTTPX основан на дизайне Requests.

View File

@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip
</div> </div>
/// tip | Подсказка
Иногда при попытке обновить pip вы можете получить ошибку **`No module named pip`**.
Если это произошло, установите и обновите pip с помощью команды ниже:
<div class="termy">
```console
$ python -m ensurepip --upgrade
---> 100%
```
</div>
Эта команда установит pip, если он ещё не установлен, а также гарантирует, что установленная версия pip будет не старее, чем версия, доступная в `ensurepip`.
///
## Добавление `.gitignore` { #add-gitignore } ## Добавление `.gitignore` { #add-gitignore }
Если вы используете **Git** (а вам стоит его использовать), добавьте файл `.gitignore`, чтобы исключить из Git всё, что находится в вашей `.venv`. Если вы используете **Git** (а вам стоит его использовать), добавьте файл `.gitignore`, чтобы исключить из Git всё, что находится в вашей `.venv`.
@ -834,7 +854,7 @@ I solemnly swear 🐺
* Управлять **виртуальным окружением** ваших проектов * Управлять **виртуальным окружением** ваших проектов
* Устанавливать **пакеты** * Устанавливать **пакеты**
* Управлять **зависимостями и версиями** пакетов вашего проекта * Управлять **зависимостями и версиями** пакетов вашего проекта
* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшне точно так же, как и на компьютере при разработке — это называется **locking** * Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшн точно так же, как и на компьютере при разработке — это называется **locking**
* И многое другое * И многое другое
## Заключение { #conclusion } ## Заключение { #conclusion }

View File

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

View File

@ -18,7 +18,7 @@ from typing import (
from fastapi._compat import may_v1, shared from fastapi._compat import may_v1, shared
from fastapi.openapi.constants import REF_TEMPLATE from fastapi.openapi.constants import REF_TEMPLATE
from fastapi.types import IncEx, ModelNameMap, UnionType from fastapi.types import IncEx, ModelNameMap, UnionType
from pydantic import BaseModel, ConfigDict, TypeAdapter, create_model from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
from pydantic import ValidationError as ValidationError from pydantic import ValidationError as ValidationError
@ -50,6 +50,45 @@ UndefinedType = PydanticUndefinedType
evaluate_forwardref = eval_type_lenient evaluate_forwardref = eval_type_lenient
Validator = Any Validator = Any
# TODO: remove when dropping support for Pydantic < v2.12.3
_Attrs = {
"default": ...,
"default_factory": None,
"alias": None,
"alias_priority": None,
"validation_alias": None,
"serialization_alias": None,
"title": None,
"field_title_generator": None,
"description": None,
"examples": None,
"exclude": None,
"exclude_if": None,
"discriminator": None,
"deprecated": None,
"json_schema_extra": None,
"frozen": None,
"validate_default": None,
"repr": True,
"init": None,
"init_var": None,
"kw_only": None,
}
# TODO: remove when dropping support for Pydantic < v2.12.3
def asdict(field_info: FieldInfo) -> Dict[str, Any]:
attributes = {}
for attr in _Attrs:
value = getattr(field_info, attr, Undefined)
if value is not Undefined:
attributes[attr] = value
return {
"annotation": field_info.annotation,
"metadata": field_info.metadata,
"attributes": attributes,
}
class BaseConfig: class BaseConfig:
pass pass
@ -71,6 +110,18 @@ class ModelField:
a = self.field_info.alias a = self.field_info.alias
return a if a is not None else self.name return a if a is not None else self.name
@property
def validation_alias(self) -> Union[str, None]:
va = self.field_info.validation_alias
if isinstance(va, str) and va:
return va
return None
@property
def serialization_alias(self) -> Union[str, None]:
sa = self.field_info.serialization_alias
return sa or None
@property @property
def required(self) -> bool: def required(self) -> bool:
return self.field_info.is_required() return self.field_info.is_required()
@ -95,10 +146,15 @@ class ModelField:
warnings.simplefilter( warnings.simplefilter(
"ignore", category=UnsupportedFieldAttributeWarning "ignore", category=UnsupportedFieldAttributeWarning
) )
# TODO: remove after dropping support for Python 3.8 and
# setting the min Pydantic to v2.12.3 that adds asdict()
field_dict = asdict(self.field_info)
annotated_args = ( annotated_args = (
self.field_info.annotation, field_dict["annotation"],
*self.field_info.metadata, *field_dict["metadata"],
self.field_info, # this FieldInfo needs to be created again so that it doesn't include
# the old field info metadata and only the rest of the attributes
Field(**field_dict["attributes"]),
) )
self._type_adapter: TypeAdapter[Any] = TypeAdapter( self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[annotated_args], Annotated[annotated_args],
@ -207,12 +263,17 @@ def get_schema_from_model_field(
if (separate_input_output_schemas or _has_computed_fields(field)) if (separate_input_output_schemas or _has_computed_fields(field))
else "validation" else "validation"
) )
field_alias = (
(field.validation_alias or field.alias)
if field.mode == "validation"
else (field.serialization_alias or field.alias)
)
# This expects that GenerateJsonSchema was already used to generate the definitions # This expects that GenerateJsonSchema was already used to generate the definitions
json_schema = field_mapping[(field, override_mode or field.mode)] json_schema = field_mapping[(field, override_mode or field.mode)]
if "$ref" not in json_schema: if "$ref" not in json_schema:
# TODO remove when deprecating Pydantic v1 # TODO remove when deprecating Pydantic v1
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
json_schema["title"] = field.field_info.title or field.alias.title().replace( json_schema["title"] = field.field_info.title or field_alias.title().replace(
"_", " " "_", " "
) )
return json_schema return json_schema

View File

@ -752,7 +752,7 @@ def _validate_value_with_model_field(
def _get_multidict_value( def _get_multidict_value(
field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None
) -> Any: ) -> Any:
alias = alias or field.alias alias = alias or get_validation_alias(field)
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)): if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
value = values.getlist(alias) value = values.getlist(alias)
else: else:
@ -809,15 +809,13 @@ def request_params_to_args(
field.field_info, "convert_underscores", default_convert_underscores field.field_info, "convert_underscores", default_convert_underscores
) )
if convert_underscores: if convert_underscores:
alias = ( alias = get_validation_alias(field)
field.alias if alias == field.name:
if field.alias != field.name alias = alias.replace("_", "-")
else field.name.replace("_", "-")
)
value = _get_multidict_value(field, received_params, alias=alias) value = _get_multidict_value(field, received_params, alias=alias)
if value is not None: if value is not None:
params_to_process[field.alias] = value params_to_process[get_validation_alias(field)] = value
processed_keys.add(alias or field.alias) processed_keys.add(alias or get_validation_alias(field))
for key in received_params.keys(): for key in received_params.keys():
if key not in processed_keys: if key not in processed_keys:
@ -847,7 +845,7 @@ def request_params_to_args(
assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), ( assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
"Params must be subclasses of Param" "Params must be subclasses of Param"
) )
loc = (field_info.in_.value, field.alias) loc = (field_info.in_.value, get_validation_alias(field))
v_, errors_ = _validate_value_with_model_field( v_, errors_ = _validate_value_with_model_field(
field=field, value=value, values=values, loc=loc field=field, value=value, values=values, loc=loc
) )
@ -936,8 +934,8 @@ async def _extract_form_body(
tg.start_soon(process_fn, sub_value.read) tg.start_soon(process_fn, sub_value.read)
value = serialize_sequence_value(field=field, value=results) value = serialize_sequence_value(field=field, value=results)
if value is not None: if value is not None:
values[field.alias] = value values[get_validation_alias(field)] = value
field_aliases = {field.alias for field in body_fields} field_aliases = {get_validation_alias(field) for field in body_fields}
for key in received_body.keys(): for key in received_body.keys():
if key not in field_aliases: if key not in field_aliases:
param_values = received_body.getlist(key) param_values = received_body.getlist(key)
@ -979,11 +977,11 @@ async def request_body_to_args(
) )
return {first_field.name: v_}, errors_ return {first_field.name: v_}, errors_
for field in body_fields: for field in body_fields:
loc = ("body", field.alias) loc = ("body", get_validation_alias(field))
value: Optional[Any] = None value: Optional[Any] = None
if body_to_process is not None: if body_to_process is not None:
try: try:
value = body_to_process.get(field.alias) value = body_to_process.get(get_validation_alias(field))
# If the received body is a list, not a dict # If the received body is a list, not a dict
except AttributeError: except AttributeError:
errors.append(get_missing_field_error(loc)) errors.append(get_missing_field_error(loc))
@ -1062,3 +1060,8 @@ def get_body_field(
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
) )
return final_field return final_field
def get_validation_alias(field: ModelField) -> str:
va = getattr(field, "validation_alias", None)
return va or field.alias

View File

@ -19,6 +19,7 @@ from fastapi.dependencies.utils import (
_get_flat_fields_from_params, _get_flat_fields_from_params,
get_flat_dependant, get_flat_dependant,
get_flat_params, get_flat_params,
get_validation_alias,
) )
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
@ -141,7 +142,7 @@ def _get_openapi_operation_parameters(
field_mapping=field_mapping, field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas, separate_input_output_schemas=separate_input_output_schemas,
) )
name = param.alias name = get_validation_alias(param)
convert_underscores = getattr( convert_underscores = getattr(
param.field_info, param.field_info,
"convert_underscores", "convert_underscores",
@ -149,7 +150,7 @@ def _get_openapi_operation_parameters(
) )
if ( if (
param_type == ParamTypes.header param_type == ParamTypes.header
and param.alias == param.name and name == param.name
and convert_underscores and convert_underscores
): ):
name = param.name.replace("_", "-") name = param.name.replace("_", "-")

View File

@ -115,6 +115,10 @@ class Param(FieldInfo): # type: ignore[misc]
else: else:
kwargs["deprecated"] = deprecated kwargs["deprecated"] = deprecated
if PYDANTIC_V2: if PYDANTIC_V2:
if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias
if validation_alias in (_Unset, None):
validation_alias = alias
kwargs.update( kwargs.update(
{ {
"annotation": annotation, "annotation": annotation,
@ -571,6 +575,10 @@ class Body(FieldInfo): # type: ignore[misc]
else: else:
kwargs["deprecated"] = deprecated kwargs["deprecated"] = deprecated
if PYDANTIC_V2: if PYDANTIC_V2:
if serialization_alias in (_Unset, None) and isinstance(alias, str):
serialization_alias = alias
if validation_alias in (_Unset, None):
validation_alias = alias
kwargs.update( kwargs.update(
{ {
"annotation": annotation, "annotation": annotation,

View File

@ -196,6 +196,7 @@ source = [
"tests", "tests",
"fastapi" "fastapi"
] ]
relative_files = true
context = '${CONTEXT}' context = '${CONTEXT}'
dynamic_context = "test_function" dynamic_context = "test_function"
omit = [ omit = [

View File

View File

@ -0,0 +1,483 @@
from typing import List, Union
import pytest
from dirty_equals import IsDict, IsOneOf, IsPartialDict
from fastapi import Body, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-list-str", operation_id="required_list_str")
async def read_required_list_str(p: Annotated[List[str], Body(embed=True)]):
return {"p": p}
class BodyModelRequiredListStr(BaseModel):
p: List[str]
@app.post("/model-required-list-str", operation_id="model_required_list_str")
def read_model_required_list_str(p: BodyModelRequiredListStr):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {
"items": {"type": "string"},
"title": "P",
"type": "array",
},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_missing(path: str, json: Union[dict, None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body", "p"], ["body"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
{
"detail": [
{
"loc": IsOneOf(["body", "p"], ["body"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.post("/required-list-alias", operation_id="required_list_alias")
async def read_required_list_alias(
p: Annotated[List[str], Body(embed=True, alias="p_alias")],
):
return {"p": p}
class BodyModelRequiredListAlias(BaseModel):
p: List[str] = Field(alias="p_alias")
@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
async def read_model_required_list_alias(p: BodyModelRequiredListAlias):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
"/model-required-list-alias",
],
)
def test_required_list_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {
"items": {"type": "string"},
"title": "P Alias",
"type": "array",
},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_missing(path: str, json: Union[dict, None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": ["hello", "world"]}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.post(
"/required-list-validation-alias", operation_id="required_list_validation_alias"
)
def read_required_list_validation_alias(
p: Annotated[List[str], Body(embed=True, validation_alias="p_val_alias")],
):
return {"p": p}
class BodyModelRequiredListValidationAlias(BaseModel):
p: List[str] = Field(validation_alias="p_val_alias")
@app.post(
"/model-required-list-validation-alias",
operation_id="model_required_list_validation_alias",
)
async def read_model_required_list_validation_alias(
p: BodyModelRequiredListValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"title": "P Val Alias",
"type": "array",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body"], ["body", "p_val_alias"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-list-alias-and-validation-alias",
operation_id="required_list_alias_and_validation_alias",
)
def read_required_list_alias_and_validation_alias(
p: Annotated[
List[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
],
):
return {"p": p}
class BodyModelRequiredListAliasAndValidationAlias(BaseModel):
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-required-list-alias-and-validation-alias",
operation_id="model_required_list_alias_and_validation_alias",
)
def read_model_required_list_alias_and_validation_alias(
p: BodyModelRequiredListAliasAndValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"title": "P Val Alias",
"type": "array",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_missing(path: str, json):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body"], ["body", "p_val_alias"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {"p": ["hello", "world"]}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": ["hello", "world"]})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}

View File

@ -0,0 +1,575 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import Body, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-list-str", operation_id="optional_list_str")
async def read_optional_list_str(
p: Annotated[Optional[List[str]], Body(embed=True)] = None,
):
return {"p": p}
class BodyModelOptionalListStr(BaseModel):
p: Optional[List[str]] = None
@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
async def read_model_optional_list_str(p: BodyModelOptionalListStr):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p": {"items": {"type": "string"}, "type": "array", "title": "P"},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_list_str_missing():
client = TestClient(app)
response = client.post("/optional-list-str")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_list_str_missing():
client = TestClient(app)
response = client.post("/model-optional-list-str")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.post("/optional-list-alias", operation_id="optional_list_alias")
async def read_optional_list_alias(
p: Annotated[Optional[List[str]], Body(embed=True, alias="p_alias")] = None,
):
return {"p": p}
class BodyModelOptionalListAlias(BaseModel):
p: Optional[List[str]] = Field(None, alias="p_alias")
@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
async def read_model_optional_list_alias(p: BodyModelOptionalListAlias):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias",
"/model-optional-list-alias",
],
)
def test_optional_list_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_list_alias_missing():
client = TestClient(app)
response = client.post("/optional-list-alias")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_list_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-list-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.post(
"/optional-list-validation-alias", operation_id="optional_list_validation_alias"
)
def read_optional_list_validation_alias(
p: Annotated[
Optional[List[str]], Body(embed=True, validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class BodyModelOptionalListValidationAlias(BaseModel):
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
@app.post(
"/model-optional-list-validation-alias",
operation_id="model_optional_list_validation_alias",
)
def read_model_optional_list_validation_alias(
p: BodyModelOptionalListValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_list_validation_alias_missing():
client = TestClient(app)
response = client.post("/optional-list-validation-alias")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_list_validation_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-list-validation-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-validation-alias",
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-validation-alias",
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-list-alias-and-validation-alias",
operation_id="optional_list_alias_and_validation_alias",
)
def read_optional_list_alias_and_validation_alias(
p: Annotated[
Optional[List[str]],
Body(embed=True, alias="p_alias", validation_alias="p_val_alias"),
] = None,
):
return {"p": p}
class BodyModelOptionalListAliasAndValidationAlias(BaseModel):
p: Optional[List[str]] = Field(
None, alias="p_alias", validation_alias="p_val_alias"
)
@app.post(
"/model-optional-list-alias-and-validation-alias",
operation_id="model_optional_list_alias_and_validation_alias",
)
def read_model_optional_list_alias_and_validation_alias(
p: BodyModelOptionalListAliasAndValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_list_alias_and_validation_alias_missing():
client = TestClient(app)
response = client.post("/optional-list-alias-and-validation-alias")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_list_alias_and_validation_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-list-alias-and-validation-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {
"p": [
"hello",
"world",
]
}

View File

@ -0,0 +1,544 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import Body, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-str", operation_id="optional_str")
async def read_optional_str(p: Annotated[Optional[str], Body(embed=True)] = None):
return {"p": p}
class BodyModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.post("/model-optional-str", operation_id="model_optional_str")
async def read_model_optional_str(p: BodyModelOptionalStr):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p": {"type": "string", "title": "P"},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_str_missing():
client = TestClient(app)
response = client.post("/optional-str")
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
def test_model_optional_str_missing():
client = TestClient(app)
response = client.post("/model-optional-str")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.post("/optional-alias", operation_id="optional_alias")
async def read_optional_alias(
p: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None,
):
return {"p": p}
class BodyModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.post("/model-optional-alias", operation_id="model_optional_alias")
async def read_model_optional_alias(p: BodyModelOptionalAlias):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
"/optional-alias",
"/model-optional-alias",
],
)
def test_optional_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_alias": {"type": "string", "title": "P Alias"},
},
"title": body_model_name,
"type": "object",
}
)
def test_optional_alias_missing():
client = TestClient(app)
response = client.post("/optional-alias")
assert response.status_code == 200
assert response.json() == {"p": None}
def test_model_optional_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_model_optional_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
def read_optional_validation_alias(
p: Annotated[
Optional[str], Body(embed=True, validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class BodyModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.post(
"/model-optional-validation-alias", operation_id="model_optional_validation_alias"
)
def read_model_optional_validation_alias(
p: BodyModelOptionalValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {"type": "string", "title": "P Val Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
def test_optional_validation_alias_missing():
client = TestClient(app)
response = client.post("/optional-validation-alias")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
def test_model_optional_validation_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-validation-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_model_optional_validation_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-alias-and-validation-alias",
operation_id="optional_alias_and_validation_alias",
)
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class BodyModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-optional-alias-and-validation-alias",
operation_id="model_optional_alias_and_validation_alias",
)
def read_model_optional_alias_and_validation_alias(
p: BodyModelOptionalAliasAndValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {"type": "string", "title": "P Val Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
def test_optional_alias_and_validation_alias_missing():
client = TestClient(app)
response = client.post("/optional-alias-and-validation-alias")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
def test_model_optional_alias_and_validation_alias_missing():
client = TestClient(app)
response = client.post("/model-optional-alias-and-validation-alias")
assert response.status_code == 422, response.text
assert response.json() == IsDict(
{
"detail": [
{
"input": None,
"loc": ["body"],
"msg": "Field required",
"type": "missing",
},
],
}
) | IsDict(
{
# TODO: remove when deprecating Pydantic v1
"detail": [
{
"loc": ["body"],
"msg": "field required",
"type": "value_error.missing",
},
],
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_model_optional_alias_and_validation_alias_missing_empty_dict(path: str):
client = TestClient(app)
response = client.post(path, json={})
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,472 @@
from typing import Any, Dict, Union
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import Body, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-str", operation_id="required_str")
async def read_required_str(p: Annotated[str, Body(embed=True)]):
return {"p": p}
class BodyModelRequiredStr(BaseModel):
p: str
@app.post("/model-required-str", operation_id="model_required_str")
async def read_model_required_str(p: BodyModelRequiredStr):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {"title": "P", "type": "string"},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str, json: Union[Dict[str, Any], None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body"], ["body", "p"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": IsOneOf(["body"], ["body", "p"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.post("/required-alias", operation_id="required_alias")
async def read_required_alias(
p: Annotated[str, Body(embed=True, alias="p_alias")],
):
return {"p": p}
class BodyModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.post("/model-required-alias", operation_id="model_required_alias")
async def read_model_required_alias(p: BodyModelRequiredAlias):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
"/required-alias",
"/model-required-alias",
],
)
def test_required_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {"title": "P Alias", "type": "string"},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str, json: Union[Dict[str, Any], None]):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": IsOneOf(["body", "p_alias"], ["body"]),
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.post("/required-validation-alias", operation_id="required_validation_alias")
def read_required_validation_alias(
p: Annotated[str, Body(embed=True, validation_alias="p_val_alias")],
):
return {"p": p}
class BodyModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.post(
"/model-required-validation-alias", operation_id="model_required_validation_alias"
)
def read_model_required_validation_alias(
p: BodyModelRequiredValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {"title": "P Val Alias", "type": "string"},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(
path: str, json: Union[Dict[str, Any], None]
):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body", "p_val_alias"], ["body"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-alias-and-validation-alias",
operation_id="required_alias_and_validation_alias",
)
def read_required_alias_and_validation_alias(
p: Annotated[
str, Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
],
):
return {"p": p}
class BodyModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-required-alias-and-validation-alias",
operation_id="model_required_alias_and_validation_alias",
)
def read_model_required_alias_and_validation_alias(
p: BodyModelRequiredAliasAndValidationAlias,
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {"title": "P Val Alias", "type": "string"},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize("json", [None, {}])
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(
path: str, json: Union[Dict[str, Any], None]
):
client = TestClient(app)
response = client.post(path, json=json)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": IsOneOf(["body"], ["body", "p_val_alias"]),
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, json={"p": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_alias": "hello"})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p_alias": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, json={"p_val_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}

View File

@ -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]

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,362 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import Cookie, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-str")
async def read_optional_str(p: Annotated[Optional[str], Cookie()] = None):
return {"p": p}
class CookieModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.get("/model-optional-str")
async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
"name": "p",
"in": "cookie",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "cookie",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/optional-alias")
async def read_optional_alias(
p: Annotated[Optional[str], Cookie(alias="p_alias")] = None,
):
return {"p": p}
class CookieModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.get("/model-optional-alias")
async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cookie()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
"name": "p_alias",
"in": "cookie",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "cookie",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-alias",
"/model-optional-alias",
],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
client.cookies.set("p_alias", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/optional-validation-alias")
def read_optional_validation_alias(
p: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class CookieModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-validation-alias")
def read_model_optional_validation_alias(
p: Annotated[CookieModelOptionalValidationAlias, Cookie()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "cookie",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
client.cookies.set("p_val_alias", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/optional-alias-and-validation-alias")
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class CookieModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-optional-alias-and-validation-alias")
def read_model_optional_alias_and_validation_alias(
p: Annotated[CookieModelOptionalAliasAndValidationAlias, Cookie()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "cookie",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
client.cookies.set("p_alias", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
client.cookies.set("p_val_alias", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,462 @@
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import Cookie, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-str")
async def read_required_str(p: Annotated[str, Cookie()]):
return {"p": p}
class CookieModelRequiredStr(BaseModel):
p: str
@app.get("/model-required-str")
async def read_model_required_str(p: Annotated[CookieModelRequiredStr, Cookie()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "cookie",
}
]
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["cookie", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["cookie", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/required-alias")
async def read_required_alias(p: Annotated[str, Cookie(alias="p_alias")]):
return {"p": p}
class CookieModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.get("/model-required-alias")
async def read_model_required_alias(p: Annotated[CookieModelRequiredAlias, Cookie()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "cookie",
}
]
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["cookie", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["cookie", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
"/model-required-alias",
],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["cookie", "p_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
{"p": "hello"},
),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["cookie", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
"/model-required-alias",
],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
client.cookies.set("p_alias", "hello")
response = client.get(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/required-validation-alias")
def read_required_validation_alias(
p: Annotated[str, Cookie(validation_alias="p_val_alias")],
):
return {"p": p}
class CookieModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.get("/model-required-validation-alias")
def read_model_required_validation_alias(
p: Annotated[CookieModelRequiredValidationAlias, Cookie()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "cookie",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"cookie",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["cookie", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
client.cookies.set("p_val_alias", "hello")
response = client.get(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/required-alias-and-validation-alias")
def read_required_alias_and_validation_alias(
p: Annotated[str, Cookie(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class CookieModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-alias-and-validation-alias")
def read_model_required_alias_and_validation_alias(
p: Annotated[CookieModelRequiredAliasAndValidationAlias, Cookie()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "cookie",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"cookie",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
client.cookies.set("p", "hello")
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"cookie",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
{"p": "hello"},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
client.cookies.set("p_alias", "hello")
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["cookie", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
{"p_alias": "hello"},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
client.cookies.set("p_val_alias", "hello")
response = client.get(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,557 @@
from typing import List
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, File, UploadFile
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/list-bytes", operation_id="list_bytes")
async def read_list_bytes(p: Annotated[List[bytes], File()]):
return {"file_size": [len(file) for file in p]}
@app.post("/list-uploadfile", operation_id="list_uploadfile")
async def read_list_uploadfile(p: Annotated[List[UploadFile], File()]):
return {"file_size": [file.size for file in p]}
@pytest.mark.parametrize(
"path",
[
"/list-bytes",
"/list-uploadfile",
],
)
def test_list_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P",
},
)
| IsDict(
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"title": "P",
},
)
)
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/list-bytes",
"/list-uploadfile",
],
)
def test_list_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/list-bytes",
"/list-uploadfile",
],
)
def test_list(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 200
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Alias
@app.post("/list-bytes-alias", operation_id="list_bytes_alias")
async def read_list_bytes_alias(p: Annotated[List[bytes], File(alias="p_alias")]):
return {"file_size": [len(file) for file in p]}
@app.post("/list-uploadfile-alias", operation_id="list_uploadfile_alias")
async def read_list_uploadfile_alias(
p: Annotated[List[UploadFile], File(alias="p_alias")],
):
return {"file_size": [file.size for file in p]}
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias",
"/list-uploadfile-alias",
],
)
def test_list_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Alias",
},
)
| IsDict(
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"title": "P Alias",
},
)
)
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias",
"/list-uploadfile-alias",
],
)
def test_list_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias",
"/list-uploadfile-alias",
],
)
def test_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias",
"/list-uploadfile-alias",
],
)
def test_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Validation alias
@app.post("/list-bytes-validation-alias", operation_id="list_bytes_validation_alias")
def read_list_bytes_validation_alias(
p: Annotated[List[bytes], File(validation_alias="p_val_alias")],
):
return {"file_size": [len(file) for file in p]}
@app.post(
"/list-uploadfile-validation-alias",
operation_id="list_uploadfile_validation_alias",
)
def read_list_uploadfile_validation_alias(
p: Annotated[List[UploadFile], File(validation_alias="p_val_alias")],
):
return {"file_size": [file.size for file in p]}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-validation-alias",
"/list-uploadfile-validation-alias",
],
)
def test_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Val Alias",
},
)
| IsDict(
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"title": "P Val Alias",
},
)
)
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-validation-alias",
"/list-uploadfile-validation-alias",
],
)
def test_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-validation-alias",
"/list-uploadfile-validation-alias",
],
)
def test_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-validation-alias",
"/list-uploadfile-validation-alias",
],
)
def test_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
)
assert response.status_code == 200, response.text
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Alias and validation alias
@app.post(
"/list-bytes-alias-and-validation-alias",
operation_id="list_bytes_alias_and_validation_alias",
)
def read_list_bytes_alias_and_validation_alias(
p: Annotated[List[bytes], File(alias="p_alias", validation_alias="p_val_alias")],
):
return {"file_size": [len(file) for file in p]}
@app.post(
"/list-uploadfile-alias-and-validation-alias",
operation_id="list_uploadfile_alias_and_validation_alias",
)
def read_list_uploadfile_alias_and_validation_alias(
p: Annotated[
List[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
],
):
return {"file_size": [file.size for file in p]}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias-and-validation-alias",
"/list-uploadfile-alias-and-validation-alias",
],
)
def test_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Val Alias",
},
)
| IsDict(
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"title": "P Val Alias",
},
)
)
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias-and-validation-alias",
"/list-uploadfile-alias-and-validation-alias",
],
)
def test_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias-and-validation-alias",
"/list-uploadfile-alias-and-validation-alias",
],
)
def test_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", "hello"), ("p", "world")])
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias-and-validation-alias",
"/list-uploadfile-alias-and-validation-alias",
],
)
def test_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/list-bytes-alias-and-validation-alias",
"/list-uploadfile-alias-and-validation-alias",
],
)
def test_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
)
assert response.status_code == 200, response.text
assert response.json() == {"file_size": [5, 5]}

View File

@ -0,0 +1,408 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, File, UploadFile
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-bytes", operation_id="optional_bytes")
async def read_optional_bytes(p: Annotated[Optional[bytes], File()] = None):
return {"file_size": len(p) if p else None}
@app.post("/optional-uploadfile", operation_id="optional_uploadfile")
async def read_optional_uploadfile(p: Annotated[Optional[UploadFile], File()] = None):
return {"file_size": p.size if p else None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes",
"/optional-uploadfile",
],
)
def test_optional_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": (
IsDict(
{
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "P", "type": "string", "format": "binary"}
)
),
},
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes",
"/optional-uploadfile",
],
)
def test_optional_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes",
"/optional-uploadfile",
],
)
def test_optional(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 200
assert response.json() == {"file_size": 5}
# =====================================================================================
# Alias
@app.post("/optional-bytes-alias", operation_id="optional_bytes_alias")
async def read_optional_bytes_alias(
p: Annotated[Optional[bytes], File(alias="p_alias")] = None,
):
return {"file_size": len(p) if p else None}
@app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias")
async def read_optional_uploadfile_alias(
p: Annotated[Optional[UploadFile], File(alias="p_alias")] = None,
):
return {"file_size": p.size if p else None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias",
"/optional-uploadfile-alias",
],
)
def test_optional_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": (
IsDict(
{
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "P Alias", "type": "string", "format": "binary"}
)
),
},
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias",
"/optional-uploadfile-alias",
],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias",
"/optional-uploadfile-alias",
],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 200
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias",
"/optional-uploadfile-alias",
],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5}
# =====================================================================================
# Validation alias
@app.post(
"/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias"
)
def read_optional_bytes_validation_alias(
p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None,
):
return {"file_size": len(p) if p else None}
@app.post(
"/optional-uploadfile-validation-alias",
operation_id="optional_uploadfile_validation_alias",
)
def read_optional_uploadfile_validation_alias(
p: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None,
):
return {"file_size": p.size if p else None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-validation-alias",
"/optional-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Val Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "P Val Alias", "type": "string", "format": "binary"}
)
),
},
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-validation-alias",
"/optional-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-validation-alias",
"/optional-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-validation-alias",
"/optional-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_val_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5}
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-bytes-alias-and-validation-alias",
operation_id="optional_bytes_alias_and_validation_alias",
)
def read_optional_bytes_alias_and_validation_alias(
p: Annotated[
Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"file_size": len(p) if p else None}
@app.post(
"/optional-uploadfile-alias-and-validation-alias",
operation_id="optional_uploadfile_alias_and_validation_alias",
)
def read_optional_uploadfile_alias_and_validation_alias(
p: Annotated[
Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"file_size": p.size if p else None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias-and-validation-alias",
"/optional-uploadfile-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "null"},
],
"title": "P Val Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "P Val Alias", "type": "string", "format": "binary"}
)
),
},
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias-and-validation-alias",
"/optional-uploadfile-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias-and-validation-alias",
"/optional-uploadfile-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias-and-validation-alias",
"/optional-uploadfile-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-bytes-alias-and-validation-alias",
"/optional-uploadfile-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_val_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5}

View File

@ -0,0 +1,434 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, File, UploadFile
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-list-bytes")
async def read_optional_list_bytes(p: Annotated[Optional[List[bytes]], File()] = None):
return {"file_size": [len(file) for file in p] if p else None}
@app.post("/optional-list-uploadfile")
async def read_optional_list_uploadfile(
p: Annotated[Optional[List[UploadFile]], File()] = None,
):
return {"file_size": [file.size for file in p] if p else None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes",
"/optional-list-uploadfile",
],
)
def test_optional_list_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "P",
"type": "array",
"items": {"type": "string", "format": "binary"},
},
)
),
},
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes",
"/optional-list-uploadfile",
],
)
def test_optional_list_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes",
"/optional-list-uploadfile",
],
)
def test_optional_list(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 200
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Alias
@app.post("/optional-list-bytes-alias")
async def read_optional_list_bytes_alias(
p: Annotated[Optional[List[bytes]], File(alias="p_alias")] = None,
):
return {"file_size": [len(file) for file in p] if p else None}
@app.post("/optional-list-uploadfile-alias")
async def read_optional_list_uploadfile_alias(
p: Annotated[Optional[List[UploadFile]], File(alias="p_alias")] = None,
):
return {"file_size": [file.size for file in p] if p else None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias",
"/optional-list-uploadfile-alias",
],
)
def test_optional_list_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "P Alias",
"type": "array",
"items": {"type": "string", "format": "binary"},
}
)
),
},
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias",
"/optional-list-uploadfile-alias",
],
)
def test_optional_list_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias",
"/optional-list-uploadfile-alias",
],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias",
"/optional-list-uploadfile-alias",
],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Validation alias
@app.post("/optional-list-bytes-validation-alias")
def read_optional_list_bytes_validation_alias(
p: Annotated[Optional[List[bytes]], File(validation_alias="p_val_alias")] = None,
):
return {"file_size": [len(file) for file in p] if p else None}
@app.post("/optional-list-uploadfile-validation-alias")
def read_optional_list_uploadfile_validation_alias(
p: Annotated[
Optional[List[UploadFile]], File(validation_alias="p_val_alias")
] = None,
):
return {"file_size": [file.size for file in p] if p else None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-validation-alias",
"/optional-list-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Val Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "P Val Alias",
"type": "array",
"items": {"type": "string", "format": "binary"},
}
)
),
},
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-validation-alias",
"/optional-list-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-validation-alias",
"/optional-list-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-validation-alias",
"/optional-list-uploadfile-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
)
assert response.status_code == 200, response.text
assert response.json() == {"file_size": [5, 5]}
# =====================================================================================
# Alias and validation alias
@app.post("/optional-list-bytes-alias-and-validation-alias")
def read_optional_list_bytes_alias_and_validation_alias(
p: Annotated[
Optional[List[bytes]], File(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"file_size": [len(file) for file in p] if p else None}
@app.post("/optional-list-uploadfile-alias-and-validation-alias")
def read_optional_list_uploadfile_alias_and_validation_alias(
p: Annotated[
Optional[List[UploadFile]],
File(alias="p_alias", validation_alias="p_val_alias"),
] = None,
):
return {"file_size": [file.size for file in p] if p else None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": (
IsDict(
{
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
},
{"type": "null"},
],
"title": "P Val Alias",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"title": "P Val Alias",
"type": "array",
"items": {"type": "string", "format": "binary"},
}
)
),
},
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-bytes-alias-and-validation-alias",
"/optional-list-uploadfile-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
)
assert response.status_code == 200, response.text
assert response.json() == {"file_size": [5, 5]}

View File

@ -0,0 +1,479 @@
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, File, UploadFile
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-bytes", operation_id="required_bytes")
async def read_required_bytes(p: Annotated[bytes, File()]):
return {"file_size": len(p)}
@app.post("/required-uploadfile", operation_id="required_uploadfile")
async def read_required_uploadfile(p: Annotated[UploadFile, File()]):
return {"file_size": p.size}
@pytest.mark.parametrize(
"path",
[
"/required-bytes",
"/required-uploadfile",
],
)
def test_required_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {"title": "P", "type": "string", "format": "binary"},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/required-bytes",
"/required-uploadfile",
],
)
def test_required_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-bytes",
"/required-uploadfile",
],
)
def test_required(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 200
assert response.json() == {"file_size": 5}
# =====================================================================================
# Alias
@app.post("/required-bytes-alias", operation_id="required_bytes_alias")
async def read_required_bytes_alias(p: Annotated[bytes, File(alias="p_alias")]):
return {"file_size": len(p)}
@app.post("/required-uploadfile-alias", operation_id="required_uploadfile_alias")
async def read_required_uploadfile_alias(
p: Annotated[UploadFile, File(alias="p_alias")],
):
return {"file_size": p.size}
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias",
"/required-uploadfile-alias",
],
)
def test_required_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {"title": "P Alias", "type": "string", "format": "binary"},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias",
"/required-uploadfile-alias",
],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias",
"/required-uploadfile-alias",
],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": None,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias",
"/required-uploadfile-alias",
],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5}
# =====================================================================================
# Validation alias
@app.post(
"/required-bytes-validation-alias", operation_id="required_bytes_validation_alias"
)
def read_required_bytes_validation_alias(
p: Annotated[bytes, File(validation_alias="p_val_alias")],
):
return {"file_size": len(p)}
@app.post(
"/required-uploadfile-validation-alias",
operation_id="required_uploadfile_validation_alias",
)
def read_required_uploadfile_validation_alias(
p: Annotated[UploadFile, File(validation_alias="p_val_alias")],
):
return {"file_size": p.size}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-validation-alias",
"/required-uploadfile-validation-alias",
],
)
def test_required_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"title": "P Val Alias",
"type": "string",
"format": "binary",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-validation-alias",
"/required-uploadfile-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-validation-alias",
"/required-uploadfile-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files=[("p", b"hello")])
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-validation-alias",
"/required-uploadfile-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_val_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5}
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-bytes-alias-and-validation-alias",
operation_id="required_bytes_alias_and_validation_alias",
)
def read_required_bytes_alias_and_validation_alias(
p: Annotated[bytes, File(alias="p_alias", validation_alias="p_val_alias")],
):
return {"file_size": len(p)}
@app.post(
"/required-uploadfile-alias-and-validation-alias",
operation_id="required_uploadfile_alias_and_validation_alias",
)
def read_required_uploadfile_alias_and_validation_alias(
p: Annotated[UploadFile, File(alias="p_alias", validation_alias="p_val_alias")],
):
return {"file_size": p.size}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias-and-validation-alias",
"/required-uploadfile-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"title": "P Val Alias",
"type": "string",
"format": "binary",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias-and-validation-alias",
"/required-uploadfile-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias-and-validation-alias",
"/required-uploadfile-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, files={"p": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias-and-validation-alias",
"/required-uploadfile-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_alias", b"hello")])
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": None,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-bytes-alias-and-validation-alias",
"/required-uploadfile-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, files=[("p_val_alias", b"hello")])
assert response.status_code == 200, response.text
assert response.json() == {"file_size": 5}

View File

@ -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]

View File

@ -0,0 +1,486 @@
from typing import List
import pytest
from dirty_equals import IsDict, IsOneOf, IsPartialDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-list-str", operation_id="required_list_str")
async def read_required_list_str(p: Annotated[List[str], Form()]):
return {"p": p}
class FormModelRequiredListStr(BaseModel):
p: List[str]
@app.post("/model-required-list-str", operation_id="model_required_list_str")
def read_model_required_list_str(p: Annotated[FormModelRequiredListStr, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {
"items": {"type": "string"},
"title": "P",
"type": "array",
},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
{
"detail": [
{
"loc": ["body", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.post("/required-list-alias", operation_id="required_list_alias")
async def read_required_list_alias(p: Annotated[List[str], Form(alias="p_alias")]):
return {"p": p}
class FormModelRequiredListAlias(BaseModel):
p: List[str] = Field(alias="p_alias")
@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
async def read_model_required_list_alias(
p: Annotated[FormModelRequiredListAlias, Form()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
"/model-required-list-alias",
],
)
def test_required_list_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {
"items": {"type": "string"},
"title": "P Alias",
"type": "array",
},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
"/model-required-list-alias",
],
)
def test_required_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": ["hello", "world"]}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.post(
"/required-list-validation-alias", operation_id="required_list_validation_alias"
)
def read_required_list_validation_alias(
p: Annotated[List[str], Form(validation_alias="p_val_alias")],
):
return {"p": p}
class FormModelRequiredListValidationAlias(BaseModel):
p: List[str] = Field(validation_alias="p_val_alias")
@app.post(
"/model-required-list-validation-alias",
operation_id="model_required_list_validation_alias",
)
async def read_model_required_list_validation_alias(
p: Annotated[FormModelRequiredListValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"title": "P Val Alias",
"type": "array",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-list-alias-and-validation-alias",
operation_id="required_list_alias_and_validation_alias",
)
def read_required_list_alias_and_validation_alias(
p: Annotated[List[str], Form(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class FormModelRequiredListAliasAndValidationAlias(BaseModel):
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-required-list-alias-and-validation-alias",
operation_id="model_required_list_alias_and_validation_alias",
)
def read_model_required_list_alias_and_validation_alias(
p: Annotated[FormModelRequiredListAliasAndValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"title": "P Val Alias",
"type": "array",
},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
{"p": ["hello", "world"]},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": ["hello", "world"]})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}

View File

@ -0,0 +1,429 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-list-str", operation_id="optional_list_str")
async def read_optional_list_str(
p: Annotated[Optional[List[str]], Form()] = None,
):
return {"p": p}
class FormModelOptionalListStr(BaseModel):
p: Optional[List[str]] = None
@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
async def read_model_optional_list_str(p: Annotated[FormModelOptionalListStr, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p": {"items": {"type": "string"}, "type": "array", "title": "P"},
},
"title": body_model_name,
"type": "object",
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.post("/optional-list-alias", operation_id="optional_list_alias")
async def read_optional_list_alias(
p: Annotated[Optional[List[str]], Form(alias="p_alias")] = None,
):
return {"p": p}
class FormModelOptionalListAlias(BaseModel):
p: Optional[List[str]] = Field(None, alias="p_alias")
@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
async def read_model_optional_list_alias(
p: Annotated[FormModelOptionalListAlias, Form()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias",
"/model-optional-list-alias",
],
)
def test_optional_list_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.post(
"/optional-list-validation-alias", operation_id="optional_list_validation_alias"
)
def read_optional_list_validation_alias(
p: Annotated[Optional[List[str]], Form(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class FormModelOptionalListValidationAlias(BaseModel):
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
@app.post(
"/model-optional-list-validation-alias",
operation_id="model_optional_list_validation_alias",
)
def read_model_optional_list_validation_alias(
p: Annotated[FormModelOptionalListValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-validation-alias",
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-list-alias-and-validation-alias",
operation_id="optional_list_alias_and_validation_alias",
)
def read_optional_list_alias_and_validation_alias(
p: Annotated[
Optional[List[str]], Form(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class FormModelOptionalListAliasAndValidationAlias(BaseModel):
p: Optional[List[str]] = Field(
None, alias="p_alias", validation_alias="p_val_alias"
)
@app.post(
"/model-optional-list-alias-and-validation-alias",
operation_id="model_optional_list_alias_and_validation_alias",
)
def read_model_optional_list_alias_and_validation_alias(
p: Annotated[FormModelOptionalListAliasAndValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {
"items": {"type": "string"},
"type": "array",
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": ["hello", "world"]})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
assert response.status_code == 200, response.text
assert response.json() == {
"p": [
"hello",
"world",
]
}

View File

@ -0,0 +1,394 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/optional-str", operation_id="optional_str")
async def read_optional_str(p: Annotated[Optional[str], Form()] = None):
return {"p": p}
class FormModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.post("/model-optional-str", operation_id="model_optional_str")
async def read_model_optional_str(p: Annotated[FormModelOptionalStr, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p": {"type": "string", "title": "P"},
},
"title": body_model_name,
"type": "object",
}
)
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.post("/optional-alias", operation_id="optional_alias")
async def read_optional_alias(
p: Annotated[Optional[str], Form(alias="p_alias")] = None,
):
return {"p": p}
class FormModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.post("/model-optional-alias", operation_id="model_optional_alias")
async def read_model_optional_alias(p: Annotated[FormModelOptionalAlias, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
"/optional-alias",
"/model-optional-alias",
],
)
def test_optional_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_alias": {"type": "string", "title": "P Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
def read_optional_validation_alias(
p: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class FormModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.post(
"/model-optional-validation-alias", operation_id="model_optional_validation_alias"
)
def read_model_optional_validation_alias(
p: Annotated[FormModelOptionalValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {"type": "string", "title": "P Val Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.post(
"/optional-alias-and-validation-alias",
operation_id="optional_alias_and_validation_alias",
)
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Form(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class FormModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-optional-alias-and-validation-alias",
operation_id="model_optional_alias_and_validation_alias",
)
def read_model_optional_alias_and_validation_alias(
p: Annotated[FormModelOptionalAliasAndValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
{
"properties": {
"p_val_alias": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
},
"title": body_model_name,
"type": "object",
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"properties": {
"p_val_alias": {"type": "string", "title": "P Val Alias"},
},
"title": body_model_name,
"type": "object",
}
)
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,464 @@
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
from .utils import get_body_model_name
app = FastAPI()
# =====================================================================================
# Without aliases
@app.post("/required-str", operation_id="required_str")
async def read_required_str(p: Annotated[str, Form()]):
return {"p": p}
class FormModelRequiredStr(BaseModel):
p: str
@app.post("/model-required-str", operation_id="model_required_str")
async def read_model_required_str(p: Annotated[FormModelRequiredStr, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {"title": "P", "type": "string"},
},
"required": ["p"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.post("/required-alias", operation_id="required_alias")
async def read_required_alias(p: Annotated[str, Form(alias="p_alias")]):
return {"p": p}
class FormModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.post("/model-required-alias", operation_id="model_required_alias")
async def read_model_required_alias(p: Annotated[FormModelRequiredAlias, Form()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
[
"/required-alias",
"/model-required-alias",
],
)
def test_required_str_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {"title": "P Alias", "type": "string"},
},
"required": ["p_alias"],
"title": body_model_name,
"type": "object",
}
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["body", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["body", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.post("/required-validation-alias", operation_id="required_validation_alias")
def read_required_validation_alias(
p: Annotated[str, Form(validation_alias="p_val_alias")],
):
return {"p": p}
class FormModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.post(
"/model-required-validation-alias", operation_id="model_required_validation_alias"
)
def read_model_required_validation_alias(
p: Annotated[FormModelRequiredValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {"title": "P Val Alias", "type": "string"},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.post(
"/required-alias-and-validation-alias",
operation_id="required_alias_and_validation_alias",
)
def read_required_alias_and_validation_alias(
p: Annotated[str, Form(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class FormModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.post(
"/model-required-alias-and-validation-alias",
operation_id="model_required_alias_and_validation_alias",
)
def read_model_required_alias_and_validation_alias(
p: Annotated[FormModelRequiredAliasAndValidationAlias, Form()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
openapi = app.openapi()
body_model_name = get_body_model_name(openapi, path)
assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_val_alias": {"title": "P Val Alias", "type": "string"},
},
"required": ["p_val_alias"],
"title": body_model_name,
"type": "object",
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.post(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.post(path, data={"p": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"body",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_alias": "hello"})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["body", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p_alias": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.post(path, data={"p_val_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}

View File

@ -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]

View File

@ -0,0 +1,468 @@
from typing import List
import pytest
from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-list-str")
async def read_required_list_str(p: Annotated[List[str], Header()]):
return {"p": p}
class HeaderModelRequiredListStr(BaseModel):
p: List[str]
@app.get("/model-required-list-str")
def read_model_required_list_str(p: Annotated[HeaderModelRequiredListStr, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P",
"type": "array",
"items": {"type": "string"},
},
"name": "p",
"in": "header",
}
]
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p"],
"msg": "Field required",
"input": AnyThing,
}
]
}
) | IsDict(
{
"detail": [
{
"loc": ["header", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.get("/required-list-alias")
async def read_required_list_alias(p: Annotated[List[str], Header(alias="p_alias")]):
return {"p": p}
class HeaderModelRequiredListAlias(BaseModel):
p: List[str] = Field(alias="p_alias")
@app.get("/model-required-list-alias")
async def read_model_required_list_alias(
p: Annotated[HeaderModelRequiredListAlias, Header()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_alias",
"in": "header",
}
]
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p_alias"],
"msg": "Field required",
"input": AnyThing,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
"/model-required-list-alias",
],
)
def test_required_list_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
"/model-required-list-alias",
],
)
def test_required_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.get("/required-list-validation-alias")
def read_required_list_validation_alias(
p: Annotated[List[str], Header(validation_alias="p_val_alias")],
):
return {"p": p}
class HeaderModelRequiredListValidationAlias(BaseModel):
p: List[str] = Field(validation_alias="p_val_alias")
@app.get("/model-required-list-validation-alias")
async def read_model_required_list_validation_alias(
p: Annotated[HeaderModelRequiredListValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Val Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias",
],
"msg": "Field required",
"input": AnyThing,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["header", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
)
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.get("/required-list-alias-and-validation-alias")
def read_required_list_alias_and_validation_alias(
p: Annotated[List[str], Header(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class HeaderModelRequiredListAliasAndValidationAlias(BaseModel):
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-list-alias-and-validation-alias")
def read_model_required_list_alias_and_validation_alias(
p: Annotated[HeaderModelRequiredListAliasAndValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Val Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias",
],
"msg": "Field required",
"input": AnyThing,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
IsPartialDict({"p": ["hello", "world"]}),
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["header", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
IsPartialDict({"p_alias": ["hello", "world"]}),
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
)
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}

View File

@ -0,0 +1,384 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-list-str")
async def read_optional_list_str(
p: Annotated[Optional[List[str]], Header()] = None,
):
return {"p": p}
class HeaderModelOptionalListStr(BaseModel):
p: Optional[List[str]] = None
@app.get("/model-optional-list-str")
async def read_model_optional_list_str(
p: Annotated[HeaderModelOptionalListStr, Header()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P",
},
"name": "p",
"in": "header",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
"name": "p",
"in": "header",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.get("/optional-list-alias")
async def read_optional_list_alias(
p: Annotated[Optional[List[str]], Header(alias="p_alias")] = None,
):
return {"p": p}
class HeaderModelOptionalListAlias(BaseModel):
p: Optional[List[str]] = Field(None, alias="p_alias")
@app.get("/model-optional-list-alias")
async def read_model_optional_list_alias(
p: Annotated[HeaderModelOptionalListAlias, Header()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Alias",
},
"name": "p_alias",
"in": "header",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {
"items": {"type": "string"},
"type": "array",
"title": "P Alias",
},
"name": "p_alias",
"in": "header",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias",
"/model-optional-list-alias",
],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.get("/optional-list-validation-alias")
def read_optional_list_validation_alias(
p: Annotated[Optional[List[str]], Header(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class HeaderModelOptionalListValidationAlias(BaseModel):
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-list-validation-alias")
def read_model_optional_list_validation_alias(
p: Annotated[HeaderModelOptionalListValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-validation-alias",
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
)
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.get("/optional-list-alias-and-validation-alias")
def read_optional_list_alias_and_validation_alias(
p: Annotated[
Optional[List[str]], Header(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class HeaderModelOptionalListAliasAndValidationAlias(BaseModel):
p: Optional[List[str]] = Field(
None, alias="p_alias", validation_alias="p_val_alias"
)
@app.get("/model-optional-list-alias-and-validation-alias")
def read_model_optional_list_alias_and_validation_alias(
p: Annotated[HeaderModelOptionalListAliasAndValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
)
assert response.status_code == 200, response.text
assert response.json() == {
"p": [
"hello",
"world",
]
}

View File

@ -0,0 +1,354 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-str")
async def read_optional_str(p: Annotated[Optional[str], Header()] = None):
return {"p": p}
class HeaderModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.get("/model-optional-str")
async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
"name": "p",
"in": "header",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "header",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/optional-alias")
async def read_optional_alias(
p: Annotated[Optional[str], Header(alias="p_alias")] = None,
):
return {"p": p}
class HeaderModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.get("/model-optional-alias")
async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
"name": "p_alias",
"in": "header",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "header",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-alias",
"/model-optional-alias",
],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/optional-validation-alias")
def read_optional_validation_alias(
p: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class HeaderModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-validation-alias")
def read_model_optional_validation_alias(
p: Annotated[HeaderModelOptionalValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/optional-alias-and-validation-alias")
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Header(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class HeaderModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-optional-alias-and-validation-alias")
def read_model_optional_alias_and_validation_alias(
p: Annotated[HeaderModelOptionalAliasAndValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_val_alias": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,451 @@
import pytest
from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
from fastapi import FastAPI, Header
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-str")
async def read_required_str(p: Annotated[str, Header()]):
return {"p": p}
class HeaderModelRequiredStr(BaseModel):
p: str
@app.get("/model-required-str")
async def read_model_required_str(p: Annotated[HeaderModelRequiredStr, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "header",
}
]
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p"],
"msg": "Field required",
"input": AnyThing,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/required-alias")
async def read_required_alias(p: Annotated[str, Header(alias="p_alias")]):
return {"p": p}
class HeaderModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.get("/model-required-alias")
async def read_model_required_alias(p: Annotated[HeaderModelRequiredAlias, Header()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "header",
}
]
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p_alias"],
"msg": "Field required",
"input": AnyThing,
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
"/model-required-alias",
],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["header", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": "hello"})),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["header", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
"/model-required-alias",
],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/required-validation-alias")
def read_required_validation_alias(
p: Annotated[str, Header(validation_alias="p_val_alias")],
):
return {"p": p}
class HeaderModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.get("/model-required-validation-alias")
def read_model_required_validation_alias(
p: Annotated[HeaderModelRequiredValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias",
],
"msg": "Field required",
"input": AnyThing,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["header", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, IsPartialDict({"p": "hello"})),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_val_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/required-alias-and-validation-alias")
def read_required_alias_and_validation_alias(
p: Annotated[str, Header(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class HeaderModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-alias-and-validation-alias")
def read_model_required_alias_and_validation_alias(
p: Annotated[HeaderModelRequiredAliasAndValidationAlias, Header()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "header",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias",
],
"msg": "Field required",
"input": AnyThing,
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(path, headers={"p": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"header",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
IsPartialDict({"p": "hello"}),
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_alias": "hello"})
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["header", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
IsPartialDict({"p_alias": "hello"}),
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(path, headers={"p_val_alias": "hello"})
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1 @@
# FastAPI doesn't currently support non-scalar Path parameters

View File

@ -0,0 +1 @@
# Optional Path parameters are not supported

View File

@ -0,0 +1 @@
# Optional Path parameters are not supported

View File

@ -0,0 +1,90 @@
import pytest
from fastapi import FastAPI, Path
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
@app.get("/required-str/{p}")
async def read_required_str(p: Annotated[str, Path()]):
return {"p": p}
@app.get("/required-alias/{p_alias}")
async def read_required_alias(p: Annotated[str, Path(alias="p_alias")]):
return {"p": p}
@app.get("/required-validation-alias/{p_val_alias}")
def read_required_validation_alias(
p: Annotated[str, Path(validation_alias="p_val_alias")],
):
return {"p": p}
@app.get("/required-alias-and-validation-alias/{p_val_alias}")
def read_required_alias_and_validation_alias(
p: Annotated[str, Path(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
@pytest.mark.parametrize(
("path", "expected_name", "expected_title"),
[
pytest.param("/required-str/{p}", "p", "P", id="required-str"),
pytest.param(
"/required-alias/{p_alias}", "p_alias", "P Alias", id="required-alias"
),
pytest.param(
"/required-validation-alias/{p_val_alias}",
"p_val_alias",
"P Val Alias",
id="required-validation-alias",
marks=needs_pydanticv2,
),
pytest.param(
"/required-alias-and-validation-alias/{p_val_alias}",
"p_val_alias",
"P Val Alias",
id="required-alias-and-validation-alias",
marks=needs_pydanticv2,
),
],
)
def test_schema(path: str, expected_name: str, expected_title: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": expected_title, "type": "string"},
"name": expected_name,
"in": "path",
}
]
@pytest.mark.parametrize(
"path",
[
pytest.param("/required-str", id="required-str"),
pytest.param("/required-alias", id="required-alias"),
pytest.param(
"/required-validation-alias",
id="required-validation-alias",
marks=needs_pydanticv2,
),
pytest.param(
"/required-alias-and-validation-alias",
id="required-alias-and-validation-alias",
marks=needs_pydanticv2,
),
],
)
def test_success(path: str):
client = TestClient(app)
response = client.get(f"{path}/hello")
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,469 @@
from typing import List
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-list-str")
async def read_required_list_str(p: Annotated[List[str], Query()]):
return {"p": p}
class QueryModelRequiredListStr(BaseModel):
p: List[str]
@app.get("/model-required-list-str")
def read_model_required_list_str(p: Annotated[QueryModelRequiredListStr, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P",
"type": "array",
"items": {"type": "string"},
},
"name": "p",
"in": "query",
}
]
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
{
"detail": [
{
"loc": ["query", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-list-str", "/model-required-list-str"],
)
def test_required_list_str(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.get("/required-list-alias")
async def read_required_list_alias(p: Annotated[List[str], Query(alias="p_alias")]):
return {"p": p}
class QueryModelRequiredListAlias(BaseModel):
p: List[str] = Field(alias="p_alias")
@app.get("/model-required-list-alias")
async def read_model_required_list_alias(
p: Annotated[QueryModelRequiredListAlias, Query()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_alias",
"in": "query",
}
]
@pytest.mark.parametrize(
"path",
["/required-list-alias", "/model-required-list-alias"],
)
def test_required_list_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
"/model-required-list-alias",
],
)
def test_required_list_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": ["hello", "world"]}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-list-alias",
"/model-required-list-alias",
],
)
def test_required_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello&p_alias=world")
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.get("/required-list-validation-alias")
def read_required_list_validation_alias(
p: Annotated[List[str], Query(validation_alias="p_val_alias")],
):
return {"p": p}
class QueryModelRequiredListValidationAlias(BaseModel):
p: List[str] = Field(validation_alias="p_val_alias")
@app.get("/model-required-list-validation-alias")
async def read_model_required_list_validation_alias(
p: Annotated[QueryModelRequiredListValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Val Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-validation-alias",
"/model-required-list-validation-alias",
],
)
def test_required_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["query", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": ["hello", "world"]}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-list-validation-alias", "/model-required-list-validation-alias"],
)
def test_required_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.get("/required-list-alias-and-validation-alias")
def read_required_list_alias_and_validation_alias(
p: Annotated[List[str], Query(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class QueryModelRequiredListAliasAndValidationAlias(BaseModel):
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-list-alias-and-validation-alias")
def read_model_required_list_alias_and_validation_alias(
p: Annotated[QueryModelRequiredListAliasAndValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {
"title": "P Val Alias",
"type": "array",
"items": {"type": "string"},
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
{
"p": [
"hello",
"world",
]
},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello&p_alias=world")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["query", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
{"p_alias": ["hello", "world"]},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-list-alias-and-validation-alias",
"/model-required-list-alias-and-validation-alias",
],
)
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}

View File

@ -0,0 +1,380 @@
from typing import List, Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-list-str")
async def read_optional_list_str(
p: Annotated[Optional[List[str]], Query()] = None,
):
return {"p": p}
class QueryModelOptionalListStr(BaseModel):
p: Optional[List[str]] = None
@app.get("/model-optional-list-str")
async def read_model_optional_list_str(
p: Annotated[QueryModelOptionalListStr, Query()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P",
},
"name": "p",
"in": "query",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
"name": "p",
"in": "query",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200, response.text
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-str", "/model-optional-list-str"],
)
def test_optional_list_str(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias
@app.get("/optional-list-alias")
async def read_optional_list_alias(
p: Annotated[Optional[List[str]], Query(alias="p_alias")] = None,
):
return {"p": p}
class QueryModelOptionalListAlias(BaseModel):
p: Optional[List[str]] = Field(None, alias="p_alias")
@app.get("/model-optional-list-alias")
async def read_model_optional_list_alias(
p: Annotated[QueryModelOptionalListAlias, Query()],
):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Alias",
},
"name": "p_alias",
"in": "query",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {
"items": {"type": "string"},
"type": "array",
"title": "P Alias",
},
"name": "p_alias",
"in": "query",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-list-alias", "/model-optional-list-alias"],
)
def test_optional_list_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias",
"/model-optional-list-alias",
],
)
def test_optional_list_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello&p_alias=world")
assert response.status_code == 200
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Validation alias
@app.get("/optional-list-validation-alias")
def read_optional_list_validation_alias(
p: Annotated[Optional[List[str]], Query(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class QueryModelOptionalListValidationAlias(BaseModel):
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-list-validation-alias")
def read_model_optional_list_validation_alias(
p: Annotated[QueryModelOptionalListValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-validation-alias",
"/model-optional-list-validation-alias",
],
)
def test_optional_list_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
)
def test_optional_list_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
assert response.status_code == 200, response.text
assert response.json() == {"p": ["hello", "world"]}
# =====================================================================================
# Alias and validation alias
@app.get("/optional-list-alias-and-validation-alias")
def read_optional_list_alias_and_validation_alias(
p: Annotated[
Optional[List[str]], Query(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class QueryModelOptionalListAliasAndValidationAlias(BaseModel):
p: Optional[List[str]] = Field(
None, alias="p_alias", validation_alias="p_val_alias"
)
@app.get("/model-optional-list-alias-and-validation-alias")
def read_model_optional_list_alias_and_validation_alias(
p: Annotated[QueryModelOptionalListAliasAndValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [
{"items": {"type": "string"}, "type": "array"},
{"type": "null"},
],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello&p=world")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello&p_alias=world")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-list-alias-and-validation-alias",
"/model-optional-list-alias-and-validation-alias",
],
)
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
assert response.status_code == 200, response.text
assert response.json() == {
"p": [
"hello",
"world",
]
}

View File

@ -0,0 +1,354 @@
from typing import Optional
import pytest
from dirty_equals import IsDict
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/optional-str")
async def read_optional_str(p: Optional[str] = None):
return {"p": p}
class QueryModelOptionalStr(BaseModel):
p: Optional[str] = None
@app.get("/model-optional-str")
async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P",
},
"name": "p",
"in": "query",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "query",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-str", "/model-optional-str"],
)
def test_optional_str(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/optional-alias")
async def read_optional_alias(
p: Annotated[Optional[str], Query(alias="p_alias")] = None,
):
return {"p": p}
class QueryModelOptionalAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias")
@app.get("/model-optional-alias")
async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
IsDict(
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Alias",
},
"name": "p_alias",
"in": "query",
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{
"required": False,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "query",
}
)
]
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
["/optional-alias", "/model-optional-alias"],
)
def test_optional_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": None}
@pytest.mark.parametrize(
"path",
[
"/optional-alias",
"/model-optional-alias",
],
)
def test_optional_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/optional-validation-alias")
def read_optional_validation_alias(
p: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None,
):
return {"p": p}
class QueryModelOptionalValidationAlias(BaseModel):
p: Optional[str] = Field(None, validation_alias="p_val_alias")
@app.get("/model-optional-validation-alias")
def read_model_optional_validation_alias(
p: Annotated[QueryModelOptionalValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/optional-validation-alias", "/model-optional-validation-alias"],
)
def test_optional_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-validation-alias",
"/model-optional-validation-alias",
],
)
def test_optional_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/optional-alias-and-validation-alias")
def read_optional_alias_and_validation_alias(
p: Annotated[
Optional[str], Query(alias="p_alias", validation_alias="p_val_alias")
] = None,
):
return {"p": p}
class QueryModelOptionalAliasAndValidationAlias(BaseModel):
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-optional-alias-and-validation-alias")
def read_model_optional_alias_and_validation_alias(
p: Annotated[QueryModelOptionalAliasAndValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "P Val Alias",
},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello")
assert response.status_code == 200
assert response.json() == {"p": None}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/optional-alias-and-validation-alias",
"/model-optional-alias-and-validation-alias",
],
)
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,454 @@
import pytest
from dirty_equals import IsDict, IsOneOf
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import Annotated
from tests.utils import needs_pydanticv2
app = FastAPI()
# =====================================================================================
# Without aliases
@app.get("/required-str")
async def read_required_str(p: str):
return {"p": p}
class QueryModelRequiredStr(BaseModel):
p: str
@app.get("/model-required-str")
async def read_model_required_str(p: Annotated[QueryModelRequiredStr, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P", "type": "string"},
"name": "p",
"in": "query",
}
]
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
["/required-str", "/model-required-str"],
)
def test_required_str(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 200
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias
@app.get("/required-alias")
async def read_required_alias(p: Annotated[str, Query(alias="p_alias")]):
return {"p": p}
class QueryModelRequiredAlias(BaseModel):
p: str = Field(alias="p_alias")
@app.get("/model-required-alias")
async def read_model_required_alias(p: Annotated[QueryModelRequiredAlias, Query()]):
return {"p": p.p}
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_str_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Alias", "type": "string"},
"name": "p_alias",
"in": "query",
}
]
@pytest.mark.parametrize(
"path",
["/required-alias", "/model-required-alias"],
)
def test_required_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p_alias"],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
"/model-required-alias",
],
)
def test_required_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 422
assert response.json() == IsDict(
{
"detail": [
{
"type": "missing",
"loc": ["query", "p_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
{"p": "hello"},
),
}
]
}
) | IsDict(
# TODO: remove when deprecating Pydantic v1
{
"detail": [
{
"loc": ["query", "p_alias"],
"msg": "field required",
"type": "value_error.missing",
}
]
}
)
@pytest.mark.parametrize(
"path",
[
"/required-alias",
"/model-required-alias",
],
)
def test_required_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello")
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Validation alias
@app.get("/required-validation-alias")
def read_required_validation_alias(
p: Annotated[str, Query(validation_alias="p_val_alias")],
):
return {"p": p}
class QueryModelRequiredValidationAlias(BaseModel):
p: str = Field(validation_alias="p_val_alias")
@app.get("/model-required-validation-alias")
def read_model_required_validation_alias(
p: Annotated[QueryModelRequiredValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
["/required-validation-alias", "/model-required-validation-alias"],
)
def test_required_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["query", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(None, {"p": "hello"}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-validation-alias",
"/model-required-validation-alias",
],
)
def test_required_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello")
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}
# =====================================================================================
# Alias and validation alias
@app.get("/required-alias-and-validation-alias")
def read_required_alias_and_validation_alias(
p: Annotated[str, Query(alias="p_alias", validation_alias="p_val_alias")],
):
return {"p": p}
class QueryModelRequiredAliasAndValidationAlias(BaseModel):
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
@app.get("/model-required-alias-and-validation-alias")
def read_model_required_alias_and_validation_alias(
p: Annotated[QueryModelRequiredAliasAndValidationAlias, Query()],
):
return {"p": p.p}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_schema(path: str):
assert app.openapi()["paths"][path]["get"]["parameters"] == [
{
"required": True,
"schema": {"title": "P Val Alias", "type": "string"},
"name": "p_val_alias",
"in": "query",
}
]
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_missing(path: str):
client = TestClient(app)
response = client.get(path)
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(None, {}),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_name(path: str):
client = TestClient(app)
response = client.get(f"{path}?p=hello")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": [
"query",
"p_val_alias",
],
"msg": "Field required",
"input": IsOneOf(
None,
{"p": "hello"},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_alias=hello")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "missing",
"loc": ["query", "p_val_alias"],
"msg": "Field required",
"input": IsOneOf(
None,
{"p_alias": "hello"},
),
}
]
}
@needs_pydanticv2
@pytest.mark.parametrize(
"path",
[
"/required-alias-and-validation-alias",
"/model-required-alias-and-validation-alias",
],
)
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
client = TestClient(app)
response = client.get(f"{path}?p_val_alias=hello")
assert response.status_code == 200, response.text
assert response.json() == {"p": "hello"}

View File

@ -0,0 +1,207 @@
# Ref: https://github.com/fastapi/fastapi/discussions/14495
from typing import Union
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from pydantic import BaseModel
from typing_extensions import Annotated
from .utils import needs_pydanticv2
@pytest.fixture(name="client")
def client_fixture() -> TestClient:
from fastapi import Body
from pydantic import Discriminator, Tag
class Cat(BaseModel):
pet_type: str = "cat"
meows: int
class Dog(BaseModel):
pet_type: str = "dog"
barks: float
def get_pet_type(v):
assert isinstance(v, dict)
return v.get("pet_type", "")
Pet = Annotated[
Union[Annotated[Cat, Tag("cat")], Annotated[Dog, Tag("dog")]],
Discriminator(get_pet_type),
]
app = FastAPI()
@app.post("/pet/assignment")
async def create_pet_assignment(pet: Pet = Body()):
return pet
@app.post("/pet/annotated")
async def create_pet_annotated(pet: Annotated[Pet, Body()]):
return pet
client = TestClient(app)
return client
@needs_pydanticv2
def test_union_body_discriminator_assignment(client: TestClient) -> None:
response = client.post("/pet/assignment", json={"pet_type": "cat", "meows": 5})
assert response.status_code == 200, response.text
assert response.json() == {"pet_type": "cat", "meows": 5}
@needs_pydanticv2
def test_union_body_discriminator_annotated(client: TestClient) -> None:
response = client.post("/pet/annotated", json={"pet_type": "dog", "barks": 3.5})
assert response.status_code == 200, response.text
assert response.json() == {"pet_type": "dog", "barks": 3.5}
@needs_pydanticv2
def test_openapi_schema(client: TestClient) -> None:
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/pet/assignment": {
"post": {
"summary": "Create Pet Assignment",
"operationId": "create_pet_assignment_pet_assignment_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"anyOf": [
{"$ref": "#/components/schemas/Cat"},
{"$ref": "#/components/schemas/Dog"},
],
"title": "Pet",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
"/pet/annotated": {
"post": {
"summary": "Create Pet Annotated",
"operationId": "create_pet_annotated_pet_annotated_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{"$ref": "#/components/schemas/Cat"},
{"$ref": "#/components/schemas/Dog"},
],
"title": "Pet",
}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"Cat": {
"properties": {
"pet_type": {
"type": "string",
"title": "Pet Type",
"default": "cat",
},
"meows": {"type": "integer", "title": "Meows"},
},
"type": "object",
"required": ["meows"],
"title": "Cat",
},
"Dog": {
"properties": {
"pet_type": {
"type": "string",
"title": "Pet Type",
"default": "dog",
},
"barks": {"type": "number", "title": "Barks"},
},
"type": "object",
"required": ["barks"],
"title": "Dog",
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)