From d178b95695231e908c5ce398b2655223bcad16cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Feb 2026 21:19:34 +0000 Subject: [PATCH 01/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 34492bfc58..6242d037de 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 🔨 Add Kapa.ai widget (AI chatbot). PR [#14938](https://github.com/fastapi/fastapi/pull/14938) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Python 3.9 specific files, no longer needed after updating translations. PR [#14931](https://github.com/fastapi/fastapi/pull/14931) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for JWT to prevent timing attacks. PR [#14908](https://github.com/fastapi/fastapi/pull/14908) by [@tiangolo](https://github.com/tiangolo). From 3bdf470622e44a7a804225ab5c0976d7f4aba0b0 Mon Sep 17 00:00:00 2001 From: argoarsiks Date: Thu, 19 Feb 2026 00:30:43 +0300 Subject: [PATCH 02/43] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20several=20typo?= =?UTF-8?q?s=20in=20ru=20translations=20(#14934)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: fix typos * Apply suggestion from @YuriiMotov --------- Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs/ru/docs/advanced/middleware.md | 2 +- docs/ru/docs/deployment/docker.md | 2 +- docs/ru/docs/history-design-future.md | 2 +- docs/ru/docs/tutorial/security/oauth2-jwt.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ru/docs/advanced/middleware.md b/docs/ru/docs/advanced/middleware.md index 034feae7eb..1f1a160604 100644 --- a/docs/ru/docs/advanced/middleware.md +++ b/docs/ru/docs/advanced/middleware.md @@ -83,7 +83,7 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") Поддерживаются следующие аргументы: - `minimum_size` — не сжимать GZip‑ом ответы, размер которых меньше этого минимального значения в байтах. По умолчанию — `500`. -- `compresslevel` — уровень GZip‑сжатия. Целое число от 1 до 9. По умолчанию — `9`. Более низкое значение — быстреее сжатие, но больший размер файла; более высокое значение — более медленное сжатие, но меньший размер файла. +- `compresslevel` — уровень GZip‑сжатия. Целое число от 1 до 9. По умолчанию — `9`. Более низкое значение — быстрее сжатие, но больший размер файла; более высокое значение — более медленное сжатие, но меньший размер файла. ## Другие middleware { #other-middlewares } diff --git a/docs/ru/docs/deployment/docker.md b/docs/ru/docs/deployment/docker.md index 791057fe56..5dfa211599 100644 --- a/docs/ru/docs/deployment/docker.md +++ b/docs/ru/docs/deployment/docker.md @@ -214,7 +214,7 @@ CMD ["fastapi", "run", "app/main.py", "--port", "80"] 5. Копируем директорию `./app` внутрь директории `/code`. - Так как здесь весь код, который **меняется чаще всего**, кэш Docker **вряд ли** будет использоваться для этого шагa или **последующих шагов**. + Так как здесь весь код, который **меняется чаще всего**, кэш Docker **вряд ли** будет использоваться для этого шага или **последующих шагов**. Поэтому важно разместить этот шаг **ближе к концу** `Dockerfile`, чтобы оптимизировать время сборки образа контейнера. diff --git a/docs/ru/docs/history-design-future.md b/docs/ru/docs/history-design-future.md index e2395fe8b9..5019157600 100644 --- a/docs/ru/docs/history-design-future.md +++ b/docs/ru/docs/history-design-future.md @@ -76,4 +76,4 @@ У **FastAPI** великое будущее. -И [ваш вклад в это](help-fastapi.md){.internal-link target=_blank} - очень ценнен. +И [ваш вклад в это](help-fastapi.md){.internal-link target=_blank} - очень ценен. diff --git a/docs/ru/docs/tutorial/security/oauth2-jwt.md b/docs/ru/docs/tutorial/security/oauth2-jwt.md index 7838b07df4..f7853d48f7 100644 --- a/docs/ru/docs/tutorial/security/oauth2-jwt.md +++ b/docs/ru/docs/tutorial/security/oauth2-jwt.md @@ -20,7 +20,7 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4 Но он подписан. Следовательно, когда вы получаете токен, который вы эмитировали (выдавали), вы можете убедиться, что это именно вы его эмитировали. -Таким образом, можно создать токен со сроком действия, скажем, 1 неделя. А когда пользователь вернется на следующий день с тем же токеном, вы будете знать, что он все еще авторизирован в вашей системе. +Таким образом, можно создать токен со сроком действия, скажем, 1 неделя. А когда пользователь вернется на следующий день с тем же токеном, вы будете знать, что он все еще авторизован в вашей системе. Через неделю срок действия токена истечет, пользователь не будет авторизован и ему придется заново входить в систему, чтобы получить новый токен. А если пользователь (или третье лицо) попытается модифицировать токен, чтобы изменить срок действия, вы сможете это обнаружить, поскольку подписи не будут совпадать. From c44158384c46c0ceab81dea8d34def67cb318c67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Feb 2026 21:31:13 +0000 Subject: [PATCH 03/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6242d037de..4bb2aa9908 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Translations +* ✏️ Fix several typos in ru translations. PR [#14934](https://github.com/fastapi/fastapi/pull/14934) by [@argoarsiks](https://github.com/argoarsiks). * 🌐 Update translations for ko (update-all and add-missing). PR [#14923](https://github.com/fastapi/fastapi/pull/14923) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for uk (add-missing). PR [#14922](https://github.com/fastapi/fastapi/pull/14922) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for zh-hant (update-all and add-missing). PR [#14921](https://github.com/fastapi/fastapi/pull/14921) by [@YuriiMotov](https://github.com/YuriiMotov). From cf058239d14f3957e69848c81bae42afe4270a30 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:40:05 +0100 Subject: [PATCH 04/43] =?UTF-8?q?=F0=9F=91=B7=20Always=20run=20tests=20on?= =?UTF-8?q?=20push=20to=20`master`=20branch=20and=20when=20run=20by=20sche?= =?UTF-8?q?duler=20(#14940)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 338f6c390f..d61b4add94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: test: needs: - changes - if: needs.changes.outputs.src == 'true' + if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master' strategy: matrix: os: [ windows-latest, macos-latest ] From d2c17b603d2f986d5f3800d3bf24acba8e1f485b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Feb 2026 15:40:34 +0000 Subject: [PATCH 05/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4bb2aa9908..d579a3cb0a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,7 @@ hide: ### Internal +* 👷 Always run tests on push to `master` branch and when run by scheduler. PR [#14940](https://github.com/fastapi/fastapi/pull/14940) by [@YuriiMotov](https://github.com/YuriiMotov). * 🎨 Upgrade typing syntax for Python 3.10. PR [#14932](https://github.com/fastapi/fastapi/pull/14932) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump cryptography from 46.0.4 to 46.0.5. PR [#14892](https://github.com/fastapi/fastapi/pull/14892) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 12.1.0 to 12.1.1. PR [#14899](https://github.com/fastapi/fastapi/pull/14899) by [@dependabot[bot]](https://github.com/apps/dependabot). From e8b98d21871f64234520c1e770f39e6bfea3b0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 05:01:31 -0800 Subject: [PATCH 06/43] =?UTF-8?q?=F0=9F=90=9B=20Fix=20JSON=20Schema=20for?= =?UTF-8?q?=20files,=20use=20`contentMediaType`=20instead=20of=20`format:?= =?UTF-8?q?=20binary`=20(#14953)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/json-base64-bytes.md | 63 +++++ .../tutorial/json-base64-bytes/image01.png | Bin 0 -> 72638 bytes docs/en/mkdocs.yml | 1 + docs_src/json_base64_bytes/__init__.py | 0 .../json_base64_bytes/tutorial001_py310.py | 46 ++++ fastapi/_compat/v2.py | 19 +- fastapi/datastructures.py | 2 +- pyproject.toml | 1 + .../playwright/json_base64_bytes/image01.py | 37 +++ .../test_file/test_list.py | 20 +- .../test_file/test_optional.py | 8 +- .../test_file/test_optional_list.py | 20 +- .../test_file/test_required.py | 16 +- .../test_json_base64_bytes/__init__.py | 0 .../test_tutorial001.py | 225 ++++++++++++++++++ .../test_request_files/test_tutorial001.py | 4 +- .../test_request_files/test_tutorial001_02.py | 10 +- .../test_request_files/test_tutorial001_03.py | 4 +- .../test_request_files/test_tutorial002.py | 10 +- .../test_request_files/test_tutorial003.py | 10 +- .../test_tutorial001.py | 4 +- 21 files changed, 470 insertions(+), 30 deletions(-) create mode 100644 docs/en/docs/advanced/json-base64-bytes.md create mode 100644 docs/en/docs/img/tutorial/json-base64-bytes/image01.png create mode 100644 docs_src/json_base64_bytes/__init__.py create mode 100644 docs_src/json_base64_bytes/tutorial001_py310.py create mode 100644 scripts/playwright/json_base64_bytes/image01.py create mode 100644 tests/test_tutorial/test_json_base64_bytes/__init__.py create mode 100644 tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py diff --git a/docs/en/docs/advanced/json-base64-bytes.md b/docs/en/docs/advanced/json-base64-bytes.md new file mode 100644 index 0000000000..c0dfec72b9 --- /dev/null +++ b/docs/en/docs/advanced/json-base64-bytes.md @@ -0,0 +1,63 @@ +# JSON with Bytes as Base64 { #json-with-bytes-as-base64 } + +If your app needs to receive and send JSON data, but you need to include binary data in it, you can encode it as base64. + +## Base64 vs Files { #base64-vs-files } + +Consider first if you can use [Request Files](../tutorial/request-files.md){.internal-link target=_blank} for uploading binary data and [Custom Response - FileResponse](./custom-response.md#fileresponse--fileresponse-){.internal-link target=_blank} for sending binary data, instead of encoding it in JSON. + +JSON can only contain UTF-8 encoded strings, so it can't contain raw bytes. + +Base64 can encode binary data in strings, but to do it, it needs to use more characters than the original binary data, so it would normally be less efficient than regular files. + +Use base64 only if you definitely need to include binary data in JSON, and you can't use files for that. + +## Pydantic `bytes` { #pydantic-bytes } + +You can declare a Pydantic model with `bytes` fields, and then use `val_json_bytes` in the model config to tell it to use base64 to *validate* input JSON data, as part of that validation it will decode the base64 string into bytes. + +{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:9,29:35] hl[9] *} + +If you check the `/docs`, they will show that the field `data` expects base64 encoded bytes: + +
+ +
+ +You could send a request like: + +```json +{ + "description": "Some data", + "data": "aGVsbG8=" +} +``` + +/// tip + +`aGVsbG8=` is the base64 encoding of `hello`. + +/// + +And then Pydantic will decode the base64 string and give you the original bytes in the `data` field of the model. + +You will receive a response like: + +```json +{ + "description": "Some data", + "content": "hello" +} +``` + +## Pydantic `bytes` for Output Data { #pydantic-bytes-for-output-data } + +You can also use `bytes` fields with `ser_json_bytes` in the model config for output data, and Pydantic will *serialize* the bytes as base64 when generating the JSON response. + +{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,12:16,29,38:41] hl[16] *} + +## Pydantic `bytes` for Input and Output Data { #pydantic-bytes-for-input-and-output-data } + +And of course, you can use the same model configured to use base64 to handle both input (*validate*) with `val_json_bytes` and output (*serialize*) with `ser_json_bytes` when receiving and sending JSON data. + +{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,19:26,29,44:46] hl[23:26] *} diff --git a/docs/en/docs/img/tutorial/json-base64-bytes/image01.png b/docs/en/docs/img/tutorial/json-base64-bytes/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..996732b7499a97cbb7ced1612cd87962ecc3292e GIT binary patch literal 72638 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 DataOutput: + data = "hello".encode("utf-8") + return DataOutput(description="A plumbus", data=data) + + +@app.post("/data-in-out") +def post_data_in_out(body: DataInputOutput) -> DataInputOutput: + return body diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index b83bc1b55b..0535c806f2 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -27,7 +27,7 @@ from pydantic._internal._schema_generation_shared import ( # type: ignore[attr- ) from pydantic._internal._typing_extra import eval_type_lenient from pydantic.fields import FieldInfo as FieldInfo -from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema +from pydantic.json_schema import GenerateJsonSchema as _GenerateJsonSchema from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue from pydantic_core import CoreSchema as CoreSchema from pydantic_core import PydanticUndefined @@ -40,6 +40,23 @@ RequiredParam = PydanticUndefined Undefined = PydanticUndefined evaluate_forwardref = eval_type_lenient + +class GenerateJsonSchema(_GenerateJsonSchema): + # TODO: remove when this is merged (or equivalent): https://github.com/pydantic/pydantic/pull/12841 + # and dropping support for any version of Pydantic before that one (so, in a very long time) + def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue: + json_schema = {"type": "string", "contentMediaType": "application/octet-stream"} + bytes_mode = ( + self._config.ser_json_bytes + if self.mode == "serialization" + else self._config.val_json_bytes + ) + if bytes_mode == "base64": + json_schema["contentEncoding"] = "base64" + self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes) + return json_schema + + # TODO: remove when dropping support for Pydantic < v2.12.3 _Attrs = { "default": ..., diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index c04b5f0f39..479e1a7c3b 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -139,7 +139,7 @@ class UploadFile(StarletteUploadFile): def __get_pydantic_json_schema__( cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler ) -> dict[str, Any]: - return {"type": "string", "format": "binary"} + return {"type": "string", "contentMediaType": "application/octet-stream"} @classmethod def __get_pydantic_core_schema__( diff --git a/pyproject.toml b/pyproject.toml index 1e6fda3b1c..92b4739458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -548,6 +548,7 @@ ignore = [ "docs_src/security/tutorial005_an_py39.py" = ["B904"] "docs_src/security/tutorial005_py310.py" = ["B904"] "docs_src/security/tutorial005_py39.py" = ["B904"] +"docs_src/json_base64_bytes/tutorial001_py310.py" = ["UP012"] [tool.ruff.lint.isort] known-third-party = ["fastapi", "pydantic", "starlette"] diff --git a/scripts/playwright/json_base64_bytes/image01.py b/scripts/playwright/json_base64_bytes/image01.py new file mode 100644 index 0000000000..56c57e1c32 --- /dev/null +++ b/scripts/playwright/json_base64_bytes/image01.py @@ -0,0 +1,37 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="POST /data Post Data").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/json-base64-bytes/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/json_base64_bytes/tutorial001_py310.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py index 68280fcf32..5332795f4c 100644 --- a/tests/test_request_params/test_file/test_list.py +++ b/tests/test_request_params/test_file/test_list.py @@ -37,7 +37,10 @@ def test_list_schema(path: str): "properties": { "p": { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "title": "P", }, }, @@ -115,7 +118,10 @@ def test_list_alias_schema(path: str): "properties": { "p_alias": { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "title": "P Alias", }, }, @@ -221,7 +227,10 @@ def test_list_validation_alias_schema(path: str): "properties": { "p_val_alias": { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "title": "P Val Alias", }, }, @@ -338,7 +347,10 @@ def test_list_alias_and_validation_alias_schema(path: str): "properties": { "p_val_alias": { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "title": "P Val Alias", }, }, diff --git a/tests/test_request_params/test_file/test_optional.py b/tests/test_request_params/test_file/test_optional.py index b4dc11a06a..3d1aac25e2 100644 --- a/tests/test_request_params/test_file/test_optional.py +++ b/tests/test_request_params/test_file/test_optional.py @@ -37,7 +37,7 @@ def test_optional_schema(path: str): "properties": { "p": { "anyOf": [ - {"type": "string", "format": "binary"}, + {"type": "string", "contentMediaType": "application/octet-stream"}, {"type": "null"}, ], "title": "P", @@ -109,7 +109,7 @@ def test_optional_alias_schema(path: str): "properties": { "p_alias": { "anyOf": [ - {"type": "string", "format": "binary"}, + {"type": "string", "contentMediaType": "application/octet-stream"}, {"type": "null"}, ], "title": "P Alias", @@ -200,7 +200,7 @@ def test_optional_validation_alias_schema(path: str): "properties": { "p_val_alias": { "anyOf": [ - {"type": "string", "format": "binary"}, + {"type": "string", "contentMediaType": "application/octet-stream"}, {"type": "null"}, ], "title": "P Val Alias", @@ -296,7 +296,7 @@ def test_optional_alias_and_validation_alias_schema(path: str): "properties": { "p_val_alias": { "anyOf": [ - {"type": "string", "format": "binary"}, + {"type": "string", "contentMediaType": "application/octet-stream"}, {"type": "null"}, ], "title": "P Val Alias", diff --git a/tests/test_request_params/test_file/test_optional_list.py b/tests/test_request_params/test_file/test_optional_list.py index a506ec991f..3c211b1e8e 100644 --- a/tests/test_request_params/test_file/test_optional_list.py +++ b/tests/test_request_params/test_file/test_optional_list.py @@ -41,7 +41,10 @@ def test_optional_list_schema(path: str): "anyOf": [ { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, {"type": "null"}, ], @@ -116,7 +119,10 @@ def test_optional_list_alias_schema(path: str): "anyOf": [ { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, {"type": "null"}, ], @@ -205,7 +211,10 @@ def test_optional_validation_alias_schema(path: str): "anyOf": [ { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, {"type": "null"}, ], @@ -301,7 +310,10 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): "anyOf": [ { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, {"type": "null"}, ], diff --git a/tests/test_request_params/test_file/test_required.py b/tests/test_request_params/test_file/test_required.py index a0f9d23a6b..22d6c0fffd 100644 --- a/tests/test_request_params/test_file/test_required.py +++ b/tests/test_request_params/test_file/test_required.py @@ -35,7 +35,11 @@ def test_required_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p": {"title": "P", "type": "string", "format": "binary"}, + "p": { + "title": "P", + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, "required": ["p"], "title": body_model_name, @@ -109,7 +113,11 @@ def test_required_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_alias": {"title": "P Alias", "type": "string", "format": "binary"}, + "p_alias": { + "title": "P Alias", + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, "required": ["p_alias"], "title": body_model_name, @@ -216,7 +224,7 @@ def test_required_validation_alias_schema(path: str): "p_val_alias": { "title": "P Val Alias", "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", }, }, "required": ["p_val_alias"], @@ -329,7 +337,7 @@ def test_required_alias_and_validation_alias_schema(path: str): "p_val_alias": { "title": "P Val Alias", "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", }, }, "required": ["p_val_alias"], diff --git a/tests/test_tutorial/test_json_base64_bytes/__init__.py b/tests/test_tutorial/test_json_base64_bytes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py b/tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py new file mode 100644 index 0000000000..4d70bca5f3 --- /dev/null +++ b/tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py @@ -0,0 +1,225 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[pytest.param("tutorial001_py310", marks=needs_py310)], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.json_base64_bytes.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_data(client: TestClient): + response = client.post( + "/data", + json={ + "description": "A file", + "data": "SGVsbG8sIFdvcmxkIQ==", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == {"description": "A file", "content": "Hello, World!"} + + +def test_get_data(client: TestClient): + response = client.get("/data") + assert response.status_code == 200, response.text + assert response.json() == {"description": "A plumbus", "data": "aGVsbG8="} + + +def test_post_data_in_out(client: TestClient): + response = client.post( + "/data-in-out", + json={ + "description": "A plumbus", + "data": "SGVsbG8sIFdvcmxkIQ==", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "description": "A plumbus", + "data": "SGVsbG8sIFdvcmxkIQ==", + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/data": { + "get": { + "summary": "Get Data", + "operationId": "get_data_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataOutput" + } + } + }, + } + }, + }, + "post": { + "summary": "Post Data", + "operationId": "post_data_data_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/DataInput"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/data-in-out": { + "post": { + "summary": "Post Data In Out", + "operationId": "post_data_in_out_data_in_out_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataInputOutput" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataInputOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "DataInput": { + "properties": { + "description": {"type": "string", "title": "Description"}, + "data": { + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/octet-stream", + "title": "Data", + }, + }, + "type": "object", + "required": ["description", "data"], + "title": "DataInput", + }, + "DataInputOutput": { + "properties": { + "description": {"type": "string", "title": "Description"}, + "data": { + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/octet-stream", + "title": "Data", + }, + }, + "type": "object", + "required": ["description", "data"], + "title": "DataInputOutput", + }, + "DataOutput": { + "properties": { + "description": {"type": "string", "title": "Description"}, + "data": { + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/octet-stream", + "title": "Data", + }, + }, + "type": "object", + "required": ["description", "data"], + "title": "DataOutput", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index 4d3c35d65d..797225bc2d 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -162,8 +162,8 @@ def test_openapi_schema(client: TestClient): "properties": { "file": { "title": "File", + "contentMediaType": "application/octet-stream", "type": "string", - "format": "binary", } }, }, @@ -175,7 +175,7 @@ def test_openapi_schema(client: TestClient): "file": { "title": "File", "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", } }, }, diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index f199b992ae..4e3c33818e 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -134,7 +134,10 @@ def test_openapi_schema(client: TestClient): "file": { "title": "File", "anyOf": [ - {"type": "string", "format": "binary"}, + { + "type": "string", + "contentMediaType": "application/octet-stream", + }, {"type": "null"}, ], } @@ -147,7 +150,10 @@ def test_openapi_schema(client: TestClient): "file": { "title": "File", "anyOf": [ - {"type": "string", "format": "binary"}, + { + "type": "string", + "contentMediaType": "application/octet-stream", + }, {"type": "null"}, ], } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index ce22c1b5c4..bccc617046 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -123,7 +123,7 @@ def test_openapi_schema(client: TestClient): "title": "File", "type": "string", "description": "A file read as bytes", - "format": "binary", + "contentMediaType": "application/octet-stream", } }, }, @@ -134,9 +134,9 @@ def test_openapi_schema(client: TestClient): "properties": { "file": { "title": "File", + "contentMediaType": "application/octet-stream", "type": "string", "description": "A file read as UploadFile", - "format": "binary", } }, }, diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index ebf76b3a07..123468d48f 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -195,7 +195,10 @@ def test_openapi_schema(client: TestClient): "files": { "title": "Files", "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, } }, }, @@ -207,7 +210,10 @@ def test_openapi_schema(client: TestClient): "files": { "title": "Files", "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, } }, }, diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py index f11658d27c..2f554d9489 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -165,7 +165,10 @@ def test_openapi_schema(client: TestClient): "files": { "title": "Files", "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "description": "Multiple files as bytes", } }, @@ -178,7 +181,10 @@ def test_openapi_schema(client: TestClient): "files": { "title": "Files", "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "description": "Multiple files as UploadFile", } }, diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index e2462e040b..cc10d8bec5 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -198,12 +198,12 @@ def test_openapi_schema(client: TestClient): "file": { "title": "File", "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", }, "fileb": { "title": "Fileb", + "contentMediaType": "application/octet-stream", "type": "string", - "format": "binary", }, "token": {"title": "Token", "type": "string"}, }, From 1bf99b9a112031090224bca6727b9b5fa8028d79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Feb 2026 13:01:58 +0000 Subject: [PATCH 07/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d579a3cb0a..fe70b9e87c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix JSON Schema for files, use `contentMediaType` instead of `format: binary`. PR [#14953](https://github.com/fastapi/fastapi/pull/14953) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 🔨 Add Kapa.ai widget (AI chatbot). PR [#14938](https://github.com/fastapi/fastapi/pull/14938) by [@tiangolo](https://github.com/tiangolo). From 04ff07fecd5273f4cd58d1de43a3b445c8e75852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 14:07:09 +0100 Subject: [PATCH 08/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fe70b9e87c..1b0c73dc92 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,7 +9,7 @@ hide: ### Fixes -* 🐛 Fix JSON Schema for files, use `contentMediaType` instead of `format: binary`. PR [#14953](https://github.com/fastapi/fastapi/pull/14953) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Fix JSON Schema for bytes, use `"contentMediaType": "application/octet-stream"` instead of `"format": "binary"`. PR [#14953](https://github.com/fastapi/fastapi/pull/14953) by [@tiangolo](https://github.com/tiangolo). ### Docs From c91fed958e46492be96ab8bc19a16f74c3c2048a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 14:08:06 +0100 Subject: [PATCH 09/43] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.129.?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1b0c73dc92..ad31504891 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.129.1 + ### Fixes * ♻️ Fix JSON Schema for bytes, use `"contentMediaType": "application/octet-stream"` instead of `"format": "binary"`. PR [#14953](https://github.com/fastapi/fastapi/pull/14953) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index de5a0be382..1591a81cf0 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.129.0" +__version__ = "0.129.1" from starlette import status as status From f55ab7e020701de7b5e467dad9bafcf1aeab30c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 08:16:03 -0800 Subject: [PATCH 10/43] =?UTF-8?q?=F0=9F=94=A7=20Update=20pyproject.toml,?= =?UTF-8?q?=20remove=20unneeded=20lines=20(#14956)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 1 - docs_src/python_types/tutorial005_py39.py | 2 - pyproject.toml | 213 +--------------------- 3 files changed, 3 insertions(+), 213 deletions(-) delete mode 100644 docs_src/python_types/tutorial005_py39.py diff --git a/.github/labeler.yml b/.github/labeler.yml index 57c5e1120f..2ee7541166 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -30,7 +30,6 @@ internal: - .gitignore - .pre-commit-config.yaml - pdm_build.py - - requirements*.txt - uv.lock - docs/en/data/sponsors.yml - docs/en/overrides/main.html diff --git a/docs_src/python_types/tutorial005_py39.py b/docs_src/python_types/tutorial005_py39.py deleted file mode 100644 index 6c8edb0ec4..0000000000 --- a/docs_src/python_types/tutorial005_py39.py +++ /dev/null @@ -1,2 +0,0 @@ -def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes): - return item_a, item_b, item_c, item_d, item_e diff --git a/pyproject.toml b/pyproject.toml index 92b4739458..d2a025a72e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,6 @@ Issues = "https://github.com/fastapi/fastapi/issues" Changelog = "https://fastapi.tiangolo.com/release-notes/" [project.optional-dependencies] - standard = [ "fastapi-cli[standard] >=0.0.8", # For the test client @@ -245,25 +244,16 @@ disallow_incomplete_defs = false disallow_untyped_defs = false disallow_untyped_calls = false -[tool.pytest.ini_options] +[tool.pytest] +minversion = "9.0" addopts = [ "--strict-config", "--strict-markers", "--ignore=docs_src", ] -xfail_strict = true -junit_family = "xunit2" +strict_xfail = true filterwarnings = [ "error", - # see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28 - "ignore:You seem to already have a custom.*:RuntimeWarning:trio", - # TODO: remove after upgrading SQLAlchemy to a version that includes the following changes - # https://github.com/sqlalchemy/sqlalchemy/commit/59521abcc0676e936b31a523bd968fc157fef0c2 - 'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:sqlalchemy', - # Trio 24.1.0 raises a warning from attrs - # Ref: https://github.com/python-trio/trio/pull/3054 - # Remove once there's a new version of Trio - 'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio', ] [tool.coverage.run] @@ -280,7 +270,6 @@ dynamic_context = "test_function" omit = [ "docs_src/response_model/tutorial003_04_py39.py", "docs_src/response_model/tutorial003_04_py310.py", - "docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock "docs_src/dependencies/tutorial013_an_py310.py", # temporary code example? "docs_src/dependencies/tutorial014_an_py310.py", # temporary code example? # Pydantic v1 migration, no longer tested @@ -288,202 +277,6 @@ omit = [ "docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py", "docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py", "docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py", - # TODO: remove all the ignores below when all translations use the new Python 3.10 files - "docs_src/additional_responses/tutorial001_py39.py", - "docs_src/additional_responses/tutorial003_py39.py", - "docs_src/advanced_middleware/tutorial001_py39.py", - "docs_src/advanced_middleware/tutorial002_py39.py", - "docs_src/advanced_middleware/tutorial003_py39.py", - "docs_src/app_testing/app_a_py39/main.py", - "docs_src/app_testing/app_a_py39/test_main.py", - "docs_src/app_testing/tutorial001_py39.py", - "docs_src/app_testing/tutorial002_py39.py", - "docs_src/app_testing/tutorial003_py39.py", - "docs_src/app_testing/tutorial004_py39.py", - "docs_src/async_tests/app_a_py39/main.py", - "docs_src/async_tests/app_a_py39/test_main.py", - "docs_src/authentication_error_status_code/tutorial001_an_py39.py", - "docs_src/background_tasks/tutorial001_py39.py", - "docs_src/behind_a_proxy/tutorial001_01_py39.py", - "docs_src/behind_a_proxy/tutorial001_py39.py", - "docs_src/behind_a_proxy/tutorial002_py39.py", - "docs_src/behind_a_proxy/tutorial003_py39.py", - "docs_src/behind_a_proxy/tutorial004_py39.py", - "docs_src/bigger_applications/app_an_py39/dependencies.py", - "docs_src/bigger_applications/app_an_py39/internal/admin.py", - "docs_src/bigger_applications/app_an_py39/main.py", - "docs_src/bigger_applications/app_an_py39/routers/items.py", - "docs_src/bigger_applications/app_an_py39/routers/users.py", - "docs_src/bigger_applications/app_py39/dependencies.py", - "docs_src/bigger_applications/app_py39/main.py", - "docs_src/body_nested_models/tutorial008_py39.py", - "docs_src/body_nested_models/tutorial009_py39.py", - "docs_src/conditional_openapi/tutorial001_py39.py", - "docs_src/configure_swagger_ui/tutorial001_py39.py", - "docs_src/configure_swagger_ui/tutorial002_py39.py", - "docs_src/configure_swagger_ui/tutorial003_py39.py", - "docs_src/cors/tutorial001_py39.py", - "docs_src/custom_docs_ui/tutorial001_py39.py", - "docs_src/custom_docs_ui/tutorial002_py39.py", - "docs_src/custom_response/tutorial001_py39.py", - "docs_src/custom_response/tutorial001b_py39.py", - "docs_src/custom_response/tutorial002_py39.py", - "docs_src/custom_response/tutorial003_py39.py", - "docs_src/custom_response/tutorial004_py39.py", - "docs_src/custom_response/tutorial005_py39.py", - "docs_src/custom_response/tutorial006_py39.py", - "docs_src/custom_response/tutorial006b_py39.py", - "docs_src/custom_response/tutorial006c_py39.py", - "docs_src/custom_response/tutorial007_py39.py", - "docs_src/custom_response/tutorial008_py39.py", - "docs_src/custom_response/tutorial009_py39.py", - "docs_src/custom_response/tutorial009b_py39.py", - "docs_src/custom_response/tutorial009c_py39.py", - "docs_src/custom_response/tutorial010_py39.py", - "docs_src/debugging/tutorial001_py39.py", - "docs_src/dependencies/tutorial006_an_py39.py", - "docs_src/dependencies/tutorial006_py39.py", - "docs_src/dependencies/tutorial007_py39.py", - "docs_src/dependencies/tutorial008_py39.py", - "docs_src/dependencies/tutorial008b_an_py39.py", - "docs_src/dependencies/tutorial008b_py39.py", - "docs_src/dependencies/tutorial008c_an_py39.py", - "docs_src/dependencies/tutorial008c_py39.py", - "docs_src/dependencies/tutorial008d_an_py39.py", - "docs_src/dependencies/tutorial008d_py39.py", - "docs_src/dependencies/tutorial008e_an_py39.py", - "docs_src/dependencies/tutorial008e_py39.py", - "docs_src/dependencies/tutorial010_py39.py", - "docs_src/dependencies/tutorial011_an_py39.py", - "docs_src/dependencies/tutorial011_py39.py", - "docs_src/dependencies/tutorial012_an_py39.py", - "docs_src/dependencies/tutorial012_py39.py", - "docs_src/events/tutorial001_py39.py", - "docs_src/events/tutorial002_py39.py", - "docs_src/events/tutorial003_py39.py", - "docs_src/extending_openapi/tutorial001_py39.py", - "docs_src/extra_models/tutorial004_py39.py", - "docs_src/extra_models/tutorial005_py39.py", - "docs_src/first_steps/tutorial001_py39.py", - "docs_src/first_steps/tutorial003_py39.py", - "docs_src/generate_clients/tutorial001_py39.py", - "docs_src/generate_clients/tutorial002_py39.py", - "docs_src/generate_clients/tutorial003_py39.py", - "docs_src/generate_clients/tutorial004_py39.py", - "docs_src/graphql_/tutorial001_py39.py", - "docs_src/handling_errors/tutorial001_py39.py", - "docs_src/handling_errors/tutorial002_py39.py", - "docs_src/handling_errors/tutorial003_py39.py", - "docs_src/handling_errors/tutorial004_py39.py", - "docs_src/handling_errors/tutorial005_py39.py", - "docs_src/handling_errors/tutorial006_py39.py", - "docs_src/metadata/tutorial001_1_py39.py", - "docs_src/metadata/tutorial001_py39.py", - "docs_src/metadata/tutorial002_py39.py", - "docs_src/metadata/tutorial003_py39.py", - "docs_src/metadata/tutorial004_py39.py", - "docs_src/middleware/tutorial001_py39.py", - "docs_src/openapi_webhooks/tutorial001_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial001_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial002_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial003_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial005_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial006_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial007_py39.py", - "docs_src/path_operation_configuration/tutorial002b_py39.py", - "docs_src/path_operation_configuration/tutorial006_py39.py", - "docs_src/path_params/tutorial001_py39.py", - "docs_src/path_params/tutorial002_py39.py", - "docs_src/path_params/tutorial003_py39.py", - "docs_src/path_params/tutorial003b_py39.py", - "docs_src/path_params/tutorial004_py39.py", - "docs_src/path_params/tutorial005_py39.py", - "docs_src/path_params_numeric_validations/tutorial002_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial002_py39.py", - "docs_src/path_params_numeric_validations/tutorial003_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial003_py39.py", - "docs_src/path_params_numeric_validations/tutorial004_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial004_py39.py", - "docs_src/path_params_numeric_validations/tutorial005_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial005_py39.py", - "docs_src/path_params_numeric_validations/tutorial006_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial006_py39.py", - "docs_src/python_types/tutorial001_py39.py", - "docs_src/python_types/tutorial002_py39.py", - "docs_src/python_types/tutorial003_py39.py", - "docs_src/python_types/tutorial004_py39.py", - "docs_src/python_types/tutorial005_py39.py", - "docs_src/python_types/tutorial006_py39.py", - "docs_src/python_types/tutorial007_py39.py", - "docs_src/python_types/tutorial008_py39.py", - "docs_src/python_types/tutorial008b_py39.py", - "docs_src/python_types/tutorial009_py39.py", - "docs_src/python_types/tutorial009b_py39.py", - "docs_src/python_types/tutorial009c_py39.py", - "docs_src/python_types/tutorial010_py39.py", - "docs_src/python_types/tutorial013_py39.py", - "docs_src/query_params/tutorial001_py39.py", - "docs_src/query_params/tutorial005_py39.py", - "docs_src/query_params_str_validations/tutorial005_an_py39.py", - "docs_src/query_params_str_validations/tutorial005_py39.py", - "docs_src/query_params_str_validations/tutorial006_an_py39.py", - "docs_src/query_params_str_validations/tutorial006_py39.py", - "docs_src/query_params_str_validations/tutorial012_an_py39.py", - "docs_src/query_params_str_validations/tutorial012_py39.py", - "docs_src/query_params_str_validations/tutorial013_an_py39.py", - "docs_src/query_params_str_validations/tutorial013_py39.py", - "docs_src/request_files/tutorial001_03_an_py39.py", - "docs_src/request_files/tutorial001_03_py39.py", - "docs_src/request_files/tutorial001_an_py39.py", - "docs_src/request_files/tutorial001_py39.py", - "docs_src/request_files/tutorial002_an_py39.py", - "docs_src/request_files/tutorial002_py39.py", - "docs_src/request_files/tutorial003_an_py39.py", - "docs_src/request_files/tutorial003_py39.py", - "docs_src/request_form_models/tutorial001_an_py39.py", - "docs_src/request_form_models/tutorial001_py39.py", - "docs_src/request_form_models/tutorial002_an_py39.py", - "docs_src/request_form_models/tutorial002_py39.py", - "docs_src/request_forms/tutorial001_an_py39.py", - "docs_src/request_forms/tutorial001_py39.py", - "docs_src/request_forms_and_files/tutorial001_an_py39.py", - "docs_src/request_forms_and_files/tutorial001_py39.py", - "docs_src/response_change_status_code/tutorial001_py39.py", - "docs_src/response_cookies/tutorial001_py39.py", - "docs_src/response_cookies/tutorial002_py39.py", - "docs_src/response_directly/tutorial002_py39.py", - "docs_src/response_headers/tutorial001_py39.py", - "docs_src/response_headers/tutorial002_py39.py", - "docs_src/response_model/tutorial003_02_py39.py", - "docs_src/response_model/tutorial003_03_py39.py", - "docs_src/response_status_code/tutorial001_py39.py", - "docs_src/response_status_code/tutorial002_py39.py", - "docs_src/security/tutorial001_an_py39.py", - "docs_src/security/tutorial001_py39.py", - "docs_src/security/tutorial006_an_py39.py", - "docs_src/security/tutorial006_py39.py", - "docs_src/security/tutorial007_an_py39.py", - "docs_src/security/tutorial007_py39.py", - "docs_src/settings/app01_py39/config.py", - "docs_src/settings/app01_py39/main.py", - "docs_src/settings/app02_an_py39/config.py", - "docs_src/settings/app02_an_py39/main.py", - "docs_src/settings/app02_an_py39/test_main.py", - "docs_src/settings/app02_py39/config.py", - "docs_src/settings/app02_py39/main.py", - "docs_src/settings/app02_py39/test_main.py", - "docs_src/settings/app03_an_py39/config.py", - "docs_src/settings/app03_an_py39/main.py", - "docs_src/settings/app03_py39/config.py", - "docs_src/settings/app03_py39/main.py", - "docs_src/settings/tutorial001_py39.py", - "docs_src/static_files/tutorial001_py39.py", - "docs_src/sub_applications/tutorial001_py39.py", - "docs_src/templates/tutorial001_py39.py", - "docs_src/using_request_directly/tutorial001_py39.py", - "docs_src/websockets/tutorial001_py39.py", - "docs_src/websockets/tutorial003_py39.py", - "docs_src/wsgi/tutorial001_py39.py", ] [tool.coverage.report] From 1b9a351ee844ff49cdcfa3913f38a12b24b79173 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Feb 2026 16:16:27 +0000 Subject: [PATCH 11/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ad31504891..1f348af999 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 🔧 Update pyproject.toml, remove unneeded lines. PR [#14956](https://github.com/fastapi/fastapi/pull/14956) by [@tiangolo](https://github.com/tiangolo). + ## 0.129.1 ### Fixes From 083b6ebe9efa76cdee3fe3f74ea686a2ea860f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 08:26:38 -0800 Subject: [PATCH 12/43] =?UTF-8?q?=E2=9E=96=20Drop=20support=20for=20`fasta?= =?UTF-8?q?pi-slim`,=20no=20more=20versions=20will=20be=20released,=20use?= =?UTF-8?q?=20only=20`"fastapi[standard]"`=20or=20`fastapi`=20(#14957)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 1 - pdm_build.py | 40 ---------------------------------------- pyproject.toml | 26 -------------------------- 3 files changed, 67 deletions(-) delete mode 100644 pdm_build.py diff --git a/.github/labeler.yml b/.github/labeler.yml index 2ee7541166..3c0bf473e0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -29,7 +29,6 @@ internal: - scripts/** - .gitignore - .pre-commit-config.yaml - - pdm_build.py - uv.lock - docs/en/data/sponsors.yml - docs/en/overrides/main.html diff --git a/pdm_build.py b/pdm_build.py deleted file mode 100644 index b1b662bd39..0000000000 --- a/pdm_build.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -from typing import Any - -from pdm.backend.hooks import Context - -TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE") - - -def pdm_build_initialize(context: Context) -> None: - metadata = context.config.metadata - # Get main version - version = metadata["version"] - # Get custom config for the current package, from the env var - all_configs_config: dict[str, Any] = context.config.data["tool"]["tiangolo"][ - "_internal-slim-build" - ]["packages"] - - if TIANGOLO_BUILD_PACKAGE not in all_configs_config: - return - - config = all_configs_config[TIANGOLO_BUILD_PACKAGE] - project_config: dict[str, Any] = config["project"] - # Override main [project] configs with custom configs for this package - for key, value in project_config.items(): - metadata[key] = value - # Get custom build config for the current package - build_config: dict[str, Any] = ( - config.get("tool", {}).get("pdm", {}).get("build", {}) - ) - # Override PDM build config with custom build config for this package - for key, value in build_config.items(): - context.config.build_config[key] = value - # Get main dependencies - dependencies: list[str] = metadata.get("dependencies", []) - # Sync versions in dependencies - new_dependencies = [] - for dep in dependencies: - new_dep = f"{dep}>={version}" - new_dependencies.append(new_dep) - metadata["dependencies"] = new_dependencies diff --git a/pyproject.toml b/pyproject.toml index d2a025a72e..199dc2a6fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,32 +198,6 @@ source-includes = [ "docs/en/docs/img/favicon.png", ] -[tool.tiangolo._internal-slim-build.packages.fastapi-slim.project] -name = "fastapi-slim" -readme = "fastapi-slim/README.md" -dependencies = [ - "fastapi", -] -optional-dependencies = {} -scripts = {} - -[tool.tiangolo._internal-slim-build.packages.fastapi-slim.tool.pdm.build] -# excludes needs to explicitly exclude the top level python packages, -# otherwise PDM includes them by default -# A "*" glob pattern can't be used here because in PDM internals, the patterns are put -# in a set (unordered, order varies) and each excluded file is assigned one of the -# glob patterns that matches, as the set is unordered, the matched pattern could be "*" -# independent of the order here. And then the internal code would give it a lower score -# than the one for a default included file. -# By not using "*" and explicitly excluding the top level packages, they get a higher -# score than the default inclusion -excludes = ["fastapi", "tests", "pdm_build.py"] -# source-includes needs to explicitly define some value because PDM will check the -# truthy value of the list, and if empty, will include some defaults, including "tests", -# an empty string doesn't match anything, but makes the list truthy, so that PDM -# doesn't override it during the build. -source-includes = [""] - [tool.mypy] plugins = ["pydantic.mypy"] strict = true From 69ae1d0f287acea3bf02e968f963ee6d225f3c50 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Feb 2026 16:27:01 +0000 Subject: [PATCH 13/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1f348af999..ec5869516d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ➖ Drop support for `fastapi-slim`, no more versions will be released, use only `"fastapi[standard]"` or `fastapi`. PR [#14957](https://github.com/fastapi/fastapi/pull/14957) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update pyproject.toml, remove unneeded lines. PR [#14956](https://github.com/fastapi/fastapi/pull/14956) by [@tiangolo](https://github.com/tiangolo). ## 0.129.1 From c9455d54002a1a80dec884e96ada5cffccdc32bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 08:45:15 -0800 Subject: [PATCH 14/43] =?UTF-8?q?=F0=9F=91=B7=20Fix=20CI,=20do=20not=20att?= =?UTF-8?q?empt=20to=20publish=20`fastapi-slim`=20(#14958)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2232498cb1..58f4f6dd8a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,11 +8,6 @@ on: jobs: publish: runs-on: ubuntu-latest - strategy: - matrix: - package: - - fastapi - - fastapi-slim permissions: id-token: write contents: read @@ -26,14 +21,9 @@ jobs: uses: actions/setup-python@v6 with: python-version-file: ".python-version" - # Issue ref: https://github.com/actions/setup-python/issues/436 - # cache: "pip" - # cache-dependency-path: pyproject.toml - name: Install uv uses: astral-sh/setup-uv@v7 - name: Build distribution run: uv build - env: - TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - name: Publish run: uv publish From 468d5173edf8d16342ee67f3cb433428b58dceac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Feb 2026 16:45:38 +0000 Subject: [PATCH 15/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ec5869516d..7f42cd490f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Fix CI, do not attempt to publish `fastapi-slim`. PR [#14958](https://github.com/fastapi/fastapi/pull/14958) by [@tiangolo](https://github.com/tiangolo). * ➖ Drop support for `fastapi-slim`, no more versions will be released, use only `"fastapi[standard]"` or `fastapi`. PR [#14957](https://github.com/fastapi/fastapi/pull/14957) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update pyproject.toml, remove unneeded lines. PR [#14956](https://github.com/fastapi/fastapi/pull/14956) by [@tiangolo](https://github.com/tiangolo). From 4ab8138554ed830ba543e57e810fe47f22441e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 09:22:54 -0800 Subject: [PATCH 16/43] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20pytest=20(?= =?UTF-8?q?#14959)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- uv.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 199dc2a6fc..c51eb8ce9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,7 +170,7 @@ tests = [ "mypy >=1.14.1", "pwdlib[argon2] >=0.2.1", "pyjwt >=2.9.0", - "pytest >=7.1.3,<9.0.0", + "pytest >=9.0.0", "pytest-codspeed >=4.2.0", "pyyaml >=5.3.1,<7.0.0", "sqlmodel >=0.0.31", diff --git a/uv.lock b/uv.lock index aa8c558c7d..15ca8714f6 100644 --- a/uv.lock +++ b/uv.lock @@ -1276,7 +1276,7 @@ dev = [ { name = "pydantic-ai", specifier = ">=0.4.10" }, { name = "pygithub", specifier = ">=2.8.1" }, { name = "pyjwt", specifier = ">=2.9.0" }, - { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, + { name = "pytest", specifier = ">=9.0.0" }, { name = "pytest-codspeed", specifier = ">=4.2.0" }, { name = "python-slugify", specifier = ">=8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, @@ -1329,7 +1329,7 @@ tests = [ { name = "mypy", specifier = ">=1.14.1" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, { name = "pyjwt", specifier = ">=2.9.0" }, - { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, + { name = "pytest", specifier = ">=9.0.0" }, { name = "pytest-codspeed", specifier = ">=4.2.0" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "ruff", specifier = ">=0.14.14" }, @@ -4330,7 +4330,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -4341,9 +4341,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] From f921de6495a9d770fafc62f81274bdb6c0ee1e31 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Feb 2026 17:23:22 +0000 Subject: [PATCH 17/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7f42cd490f..bf29c06135 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆️ Upgrade pytest. PR [#14959](https://github.com/fastapi/fastapi/pull/14959) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix CI, do not attempt to publish `fastapi-slim`. PR [#14958](https://github.com/fastapi/fastapi/pull/14958) by [@tiangolo](https://github.com/tiangolo). * ➖ Drop support for `fastapi-slim`, no more versions will be released, use only `"fastapi[standard]"` or `fastapi`. PR [#14957](https://github.com/fastapi/fastapi/pull/14957) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update pyproject.toml, remove unneeded lines. PR [#14956](https://github.com/fastapi/fastapi/pull/14956) by [@tiangolo](https://github.com/tiangolo). From 1e78a36b7310003f0ff634627d8a7bc53c6ccdee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 18:24:59 +0100 Subject: [PATCH 18/43] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.129.?= =?UTF-8?q?2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bf29c06135..edcb3db8d6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.129.2 + ### Internal * ⬆️ Upgrade pytest. PR [#14959](https://github.com/fastapi/fastapi/pull/14959) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 1591a81cf0..e6798ddc6d 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.129.1" +__version__ = "0.129.2" from starlette import status as status From 590a5e535587cc07041ba12d308c748433ccb168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 22 Feb 2026 08:07:19 -0800 Subject: [PATCH 19/43] =?UTF-8?q?=E2=9C=A8=20Serialize=20JSON=20response?= =?UTF-8?q?=20with=20Pydantic=20(in=20Rust),=20when=20there's=20a=20Pydant?= =?UTF-8?q?ic=20return=20type=20or=20response=20model=20(#14962)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/custom-response.md | 80 +++++-------------- docs/en/docs/advanced/response-directly.md | 28 +++++-- docs/en/docs/how-to/general.md | 4 + docs/en/docs/tutorial/response-model.md | 1 + docs_src/custom_response/tutorial010_py310.py | 6 +- fastapi/_compat/v2.py | 26 ++++++ fastapi/routing.py | 23 +++++- tests/test_dump_json_fast_path.py | 51 ++++++++++++ .../test_custom_response/test_tutorial001.py | 1 - .../test_custom_response/test_tutorial010.py | 50 ++++++++++++ 10 files changed, 196 insertions(+), 74 deletions(-) create mode 100644 tests/test_dump_json_fast_path.py create mode 100644 tests/test_tutorial/test_custom_response/test_tutorial010.py diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index 8b4b3da339..e88e958657 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -1,6 +1,6 @@ # Custom Response - HTML, Stream, File, others { #custom-response-html-stream-file-others } -By default, **FastAPI** will return the responses using `JSONResponse`. +By default, **FastAPI** will return JSON responses. You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}. @@ -10,43 +10,27 @@ But you can also declare the `Response` that you want to be used (e.g. any `Resp The contents that you return from your *path operation function* will be put inside of that `Response`. -And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*. - /// note If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs. /// -## Use `ORJSONResponse` { #use-orjsonresponse } +## JSON Responses { #json-responses } -For example, if you are squeezing performance, you can install and use `orjson` and set the response to be `ORJSONResponse`. +By default FastAPI returns JSON responses. -Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*. +If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic. -For large responses, returning a `Response` directly is much faster than returning a dictionary. +If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`. -This is because by default, FastAPI will inspect every item inside and make sure it is serializable as JSON, using the same [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} explained in the tutorial. This is what allows you to return **arbitrary objects**, for example database models. +If you declare a `response_class` with a JSON media type (`application/json`), like is the case with the `JSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*. But the data won't be serialized to JSON bytes with Pydantic, instead it will be converted with the `jsonable_encoder` and then passed to the `JSONResponse` class, which will serialize it to bytes using the standard JSON library in Python. -But if you are certain that the content that you are returning is **serializable with JSON**, you can pass it directly to the response class and avoid the extra overhead that FastAPI would have by passing your return content through the `jsonable_encoder` before passing it to the response class. +### JSON Performance { #json-performance } -{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *} +In short, if you want the maximum performance, use a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} and don't declare a `response_class` in the *path operation decorator*. -/// info - -The parameter `response_class` will also be used to define the "media type" of the response. - -In this case, the HTTP header `Content-Type` will be set to `application/json`. - -And it will be documented as such in OpenAPI. - -/// - -/// tip - -The `ORJSONResponse` is only available in FastAPI, not in Starlette. - -/// +{* ../../docs_src/response_model/tutorial001_01_py310.py ln[15:17] hl[16] *} ## HTML Response { #html-response } @@ -154,40 +138,6 @@ Takes some data and returns an `application/json` encoded response. This is the default response used in **FastAPI**, as you read above. -### `ORJSONResponse` { #orjsonresponse } - -A fast alternative JSON response using `orjson`, as you read above. - -/// info - -This requires installing `orjson` for example with `pip install orjson`. - -/// - -### `UJSONResponse` { #ujsonresponse } - -An alternative JSON response using `ujson`. - -/// info - -This requires installing `ujson` for example with `pip install ujson`. - -/// - -/// warning - -`ujson` is less careful than Python's built-in implementation in how it handles some edge-cases. - -/// - -{* ../../docs_src/custom_response/tutorial001_py310.py hl[2,7] *} - -/// tip - -It's possible that `ORJSONResponse` might be a faster alternative. - -/// - ### `RedirectResponse` { #redirectresponse } Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default. @@ -268,7 +218,7 @@ In this case, you can return the file path directly from your *path operation* f You can create your own custom response class, inheriting from `Response` and using it. -For example, let's say that you want to use `orjson`, but with some custom settings not used in the included `ORJSONResponse` class. +For example, let's say that you want to use `orjson` with some settings. Let's say you want it to return indented and formatted JSON, so you want to use the orjson option `orjson.OPT_INDENT_2`. @@ -292,13 +242,21 @@ Now instead of returning: Of course, you will probably find much better ways to take advantage of this than formatting JSON. 😉 +### `orjson` or Response Model { #orjson-or-response-model } + +If what you are looking for is performance, you are probably better off using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than an `orjson` response. + +With a response model, FastAPI will use Pydantic to serialize the data to JSON, without using intermediate steps, like converting it with `jsonable_encoder`, which would happen in any other case. + +And under the hood, Pydantic uses the same underlying Rust mechanisms as `orjson` to serialize to JSON, so you will already get the best performance with a response model. + ## Default response class { #default-response-class } When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default. The parameter that defines this is `default_response_class`. -In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`. +In the example below, **FastAPI** will use `HTMLResponse` by default, in all *path operations*, instead of JSON. {* ../../docs_src/custom_response/tutorial010_py310.py hl[2,4] *} diff --git a/docs/en/docs/advanced/response-directly.md b/docs/en/docs/advanced/response-directly.md index 76cc50d03c..9d58490eb1 100644 --- a/docs/en/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -2,19 +2,23 @@ When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc. -By default, **FastAPI** would automatically convert that return value to JSON using the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. +If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic. -Then, behind the scenes, it would put that JSON-compatible data (e.g. a `dict`) inside of a `JSONResponse` that would be used to send the response to the client. +If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`. -But you can return a `JSONResponse` directly from your *path operations*. +You could also create a `JSONResponse` directly and return it. -It might be useful, for example, to return custom headers or cookies. +/// tip + +You will normally have much better performance using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than returning a `JSONResponse` directly, as that way it serializes the data using Pydantic, in Rust. + +/// ## Return a `Response` { #return-a-response } -In fact, you can return any `Response` or any sub-class of it. +You can return any `Response` or any sub-class of it. -/// tip +/// info `JSONResponse` itself is a sub-class of `Response`. @@ -56,6 +60,18 @@ You could put your XML content in a string, put that in a `Response`, and return {* ../../docs_src/response_directly/tutorial002_py310.py hl[1,18] *} +## How a Response Model Works { #how-a-response-model-works } + +When you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} in a path operation, **FastAPI** will use it to serialize the data to JSON, using Pydantic. + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +As that will happen on the Rust side, the performance will be much better than if it was done with regular Python and the `JSONResponse` class. + +When using a response model FastAPI won't use the `jsonable_encoder` to convert the data (which would be slower) nor the `JSONResponse` class. + +Instead it takes the JSON bytes generated with Pydantic using the response model and returns a `Response` with the right media type for JSON directly (`application/json`). + ## Notes { #notes } When you return a `Response` directly its data is not validated, converted (serialized), or documented automatically. diff --git a/docs/en/docs/how-to/general.md b/docs/en/docs/how-to/general.md index 9347192607..4f611dab05 100644 --- a/docs/en/docs/how-to/general.md +++ b/docs/en/docs/how-to/general.md @@ -6,6 +6,10 @@ Here are several pointers to other places in the docs, for general or frequent q To ensure that you don't return more data than you should, read the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}. +## Optimize Response Performance - Response Model - Return Type { #optimize-response-performance-response-model-return-type } + +To optimize performance when returning JSON data, use a return type or response model, that way Pydantic will handle the serialization to JSON on the Rust side, without going through Python. Read more in the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}. + ## Documentation Tags - OpenAPI { #documentation-tags-openapi } To add tags to your *path operations*, and group them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 51492722ae..c8312d92c6 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -13,6 +13,7 @@ FastAPI will use this return type to: * Add a **JSON Schema** for the response, in the OpenAPI *path operation*. * This will be used by the **automatic docs**. * It will also be used by automatic client code generation tools. +* **Serialize** the returned data to JSON using Pydantic, which is written in **Rust**, so it will be **much faster**. But most importantly: diff --git a/docs_src/custom_response/tutorial010_py310.py b/docs_src/custom_response/tutorial010_py310.py index 57cb062604..d5bc783aa0 100644 --- a/docs_src/custom_response/tutorial010_py310.py +++ b/docs_src/custom_response/tutorial010_py310.py @@ -1,9 +1,9 @@ from fastapi import FastAPI -from fastapi.responses import ORJSONResponse +from fastapi.responses import HTMLResponse -app = FastAPI(default_response_class=ORJSONResponse) +app = FastAPI(default_response_class=HTMLResponse) @app.get("/items/") async def read_items(): - return [{"item_id": "Foo"}] + return "

Items

This is a list of items.

" diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 0535c806f2..79fba93188 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -199,6 +199,32 @@ class ModelField: exclude_none=exclude_none, ) + def serialize_json( + self, + value: Any, + *, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> bytes: + # What calls this code passes a value that already called + # self._type_adapter.validate_python(value) + # This uses Pydantic's dump_json() which serializes directly to JSON + # bytes in one pass (via Rust), avoiding the intermediate Python dict + # step of dump_python(mode="json") + json.dumps(). + return self._type_adapter.dump_json( + value, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + def __hash__(self) -> int: # Each ModelField is unique for our purposes, to allow making a dict from # ModelField to its JSON Schema. diff --git a/fastapi/routing.py b/fastapi/routing.py index ea82ab14a3..528c962965 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -271,6 +271,7 @@ async def serialize_response( exclude_none: bool = False, is_coroutine: bool = True, endpoint_ctx: EndpointContext | None = None, + dump_json: bool = False, ) -> Any: if field: if is_coroutine: @@ -286,8 +287,8 @@ async def serialize_response( body=response_content, endpoint_ctx=ctx, ) - - return field.serialize( + serializer = field.serialize_json if dump_json else field.serialize + return serializer( value, include=include, exclude=exclude, @@ -443,6 +444,14 @@ def get_request_handler( response_args["status_code"] = current_status_code if solved_result.response.status_code: response_args["status_code"] = solved_result.response.status_code + # Use the fast path (dump_json) when no custom response + # class was set and a response field with a TypeAdapter + # exists. Serializes directly to JSON bytes via Pydantic's + # Rust core, skipping the intermediate Python dict + + # json.dumps() step. + use_dump_json = response_field is not None and isinstance( + response_class, DefaultPlaceholder + ) content = await serialize_response( field=response_field, response_content=raw_response, @@ -454,8 +463,16 @@ def get_request_handler( exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, endpoint_ctx=endpoint_ctx, + dump_json=use_dump_json, ) - response = actual_response_class(content, **response_args) + if use_dump_json: + response = Response( + content=content, + media_type="application/json", + **response_args, + ) + else: + response = actual_response_class(content, **response_args) if not is_body_allowed_for_status_code(response.status_code): response.body = b"" response.headers.raw.extend(solved_result.response.headers.raw) diff --git a/tests/test_dump_json_fast_path.py b/tests/test_dump_json_fast_path.py new file mode 100644 index 0000000000..d41d5aa66f --- /dev/null +++ b/tests/test_dump_json_fast_path.py @@ -0,0 +1,51 @@ +from unittest.mock import patch + +from fastapi import FastAPI +from fastapi.responses import JSONResponse +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + price: float + + +app = FastAPI() + + +@app.get("/default") +def get_default() -> Item: + return Item(name="widget", price=9.99) + + +@app.get("/explicit", response_class=JSONResponse) +def get_explicit() -> Item: + return Item(name="widget", price=9.99) + + +client = TestClient(app) + + +def test_default_response_class_skips_json_dumps(): + """When no response_class is set, the fast path serializes directly to + JSON bytes via Pydantic's dump_json and never calls json.dumps.""" + with patch( + "starlette.responses.json.dumps", wraps=__import__("json").dumps + ) as mock_dumps: + response = client.get("/default") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + mock_dumps.assert_not_called() + + +def test_explicit_response_class_uses_json_dumps(): + """When response_class is explicitly set to JSONResponse, the normal path + is used and json.dumps is called via JSONResponse.render().""" + with patch( + "starlette.responses.json.dumps", wraps=__import__("json").dumps + ) as mock_dumps: + response = client.get("/explicit") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + mock_dumps.assert_called_once() diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index a5fe4c8f4c..cec5ebe6cb 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -9,7 +9,6 @@ from inline_snapshot import snapshot name="client", params=[ pytest.param("tutorial001_py310"), - pytest.param("tutorial010_py310"), ], ) def get_client(request: pytest.FixtureRequest): diff --git a/tests/test_tutorial/test_custom_response/test_tutorial010.py b/tests/test_tutorial/test_custom_response/test_tutorial010.py new file mode 100644 index 0000000000..ffb005cb67 --- /dev/null +++ b/tests/test_tutorial/test_custom_response/test_tutorial010.py @@ -0,0 +1,50 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial010_py310"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_response.{request.param}") + client = TestClient(mod.app) + return client + + +def test_get_custom_response(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.text == snapshot("

Items

This is a list of items.

") + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "text/html": {"schema": {"type": "string"}} + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } + ) From bc06e4296d588f39f38c7ac2fc96d3913c565962 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Feb 2026 16:07:56 +0000 Subject: [PATCH 20/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index edcb3db8d6..ba5ba420bd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return type or response model. PR [#14962](https://github.com/fastapi/fastapi/pull/14962) by [@tiangolo](https://github.com/tiangolo). + ## 0.129.2 ### Internal From eb544e704c02fd9bad34d23127cbb45255c00706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 22 Feb 2026 17:14:53 +0100 Subject: [PATCH 21/43] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.130.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ba5ba420bd..a5c5241e1b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.130.0 + ### Features * ✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return type or response model. PR [#14962](https://github.com/fastapi/fastapi/pull/14962) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index e6798ddc6d..ffa56faaf1 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.129.2" +__version__ = "0.130.0" from starlette import status as status From 2e62fb151338c39c1f122d13fe6ad255b8ce1ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 22 Feb 2026 17:18:26 +0100 Subject: [PATCH 22/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a5c5241e1b..4b7fba3368 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -12,6 +12,8 @@ hide: ### Features * ✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return type or response model. PR [#14962](https://github.com/fastapi/fastapi/pull/14962) by [@tiangolo](https://github.com/tiangolo). + * This results in 2x (or more) performance increase for JSON responses. + * New docs: [Custom Response - JSON Performance](https://fastapi.tiangolo.com/advanced/custom-response/#json-performance). ## 0.129.2 From 48e983573232eea970fb4e0261818d4ab9a481b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 22 Feb 2026 08:34:59 -0800 Subject: [PATCH 23/43] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Deprecate=20`ORJS?= =?UTF-8?q?ONResponse`=20and=20`UJSONResponse`=20(#14964)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/reference/responses.md | 8 +- fastapi/responses.py | 52 +++++++++++-- pyproject.toml | 8 +- tests/test_deprecated_responses.py | 73 +++++++++++++++++++ tests/test_orjson_response_class.py | 13 +++- .../test_custom_response/test_tutorial001.py | 2 + .../test_custom_response/test_tutorial001b.py | 10 ++- uv.lock | 20 ++++- 8 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 tests/test_deprecated_responses.py diff --git a/docs/en/docs/reference/responses.md b/docs/en/docs/reference/responses.md index bd57861294..2df53e9701 100644 --- a/docs/en/docs/reference/responses.md +++ b/docs/en/docs/reference/responses.md @@ -22,7 +22,13 @@ from fastapi.responses import ( ## FastAPI Responses -There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance. +There were a couple of custom FastAPI response classes that were intended to optimize JSON performance. + +However, they are now deprecated as you will now get better performance by using a [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/). + +That way, Pydantic will serialize the data into JSON bytes on the Rust side, which will achieve better performance than these custom JSON responses. + +Read more about it in [Custom Response - HTML, Stream, File, others - `orjson` or Response Model](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model). ::: fastapi.responses.UJSONResponse options: diff --git a/fastapi/responses.py b/fastapi/responses.py index 6c8db6f335..5b1154c046 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -1,5 +1,6 @@ from typing import Any +from fastapi.exceptions import FastAPIDeprecationWarning from starlette.responses import FileResponse as FileResponse # noqa from starlette.responses import HTMLResponse as HTMLResponse # noqa from starlette.responses import JSONResponse as JSONResponse # noqa @@ -7,6 +8,7 @@ from starlette.responses import PlainTextResponse as PlainTextResponse # noqa from starlette.responses import RedirectResponse as RedirectResponse # noqa from starlette.responses import Response as Response # noqa from starlette.responses import StreamingResponse as StreamingResponse # noqa +from typing_extensions import deprecated try: import ujson @@ -20,12 +22,29 @@ except ImportError: # pragma: nocover orjson = None # type: ignore +@deprecated( + "UJSONResponse is deprecated, FastAPI now serializes data directly to JSON " + "bytes via Pydantic when a return type or response model is set, which is " + "faster and doesn't need a custom response class. Read more in the FastAPI " + "docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model " + "and https://fastapi.tiangolo.com/tutorial/response-model/", + category=FastAPIDeprecationWarning, + stacklevel=2, +) class UJSONResponse(JSONResponse): - """ - JSON response using the high-performance ujson library to serialize data to JSON. + """JSON response using the ujson library to serialize data to JSON. - Read more about it in the - [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + **Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data + directly to JSON bytes via Pydantic when a return type or response model is + set, which is faster and doesn't need a custom response class. + + Read more in the + [FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model) + and the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + + **Note**: `ujson` is not included with FastAPI and must be installed + separately, e.g. `pip install ujson`. """ def render(self, content: Any) -> bytes: @@ -33,12 +52,29 @@ class UJSONResponse(JSONResponse): return ujson.dumps(content, ensure_ascii=False).encode("utf-8") +@deprecated( + "ORJSONResponse is deprecated, FastAPI now serializes data directly to JSON " + "bytes via Pydantic when a return type or response model is set, which is " + "faster and doesn't need a custom response class. Read more in the FastAPI " + "docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model " + "and https://fastapi.tiangolo.com/tutorial/response-model/", + category=FastAPIDeprecationWarning, + stacklevel=2, +) class ORJSONResponse(JSONResponse): - """ - JSON response using the high-performance orjson library to serialize data to JSON. + """JSON response using the orjson library to serialize data to JSON. - Read more about it in the - [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + **Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data + directly to JSON bytes via Pydantic when a return type or response model is + set, which is faster and doesn't need a custom response class. + + Read more in the + [FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model) + and the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + + **Note**: `orjson` is not included with FastAPI and must be installed + separately, e.g. `pip install orjson`. """ def render(self, content: Any) -> bytes: diff --git a/pyproject.toml b/pyproject.toml index c51eb8ce9b..79dfc1fd35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,10 +105,6 @@ all = [ "itsdangerous >=1.1.0", # For Starlette's schema generation, would not be used with FastAPI "pyyaml >=5.3.1", - # For UJSONResponse - "ujson >=5.8.0", - # For ORJSONResponse - "orjson >=3.9.3", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop @@ -151,6 +147,10 @@ docs = [ docs-tests = [ "httpx >=0.23.0,<1.0.0", "ruff >=0.14.14", + # For UJSONResponse + "ujson >=5.8.0", + # For ORJSONResponse + "orjson >=3.9.3", ] github-actions = [ "httpx >=0.27.0,<1.0.0", diff --git a/tests/test_deprecated_responses.py b/tests/test_deprecated_responses.py new file mode 100644 index 0000000000..eff5792717 --- /dev/null +++ b/tests/test_deprecated_responses.py @@ -0,0 +1,73 @@ +import warnings + +import pytest +from fastapi import FastAPI +from fastapi.exceptions import FastAPIDeprecationWarning +from fastapi.responses import ORJSONResponse, UJSONResponse +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + price: float + + +# ORJSON + + +def _make_orjson_app() -> FastAPI: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=ORJSONResponse) + + @app.get("/items") + def get_items() -> Item: + return Item(name="widget", price=9.99) + + return app + + +def test_orjson_response_returns_correct_data(): + app = _make_orjson_app() + client = TestClient(app) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + response = client.get("/items") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + + +def test_orjson_response_emits_deprecation_warning(): + with pytest.warns(FastAPIDeprecationWarning, match="ORJSONResponse is deprecated"): + ORJSONResponse(content={"hello": "world"}) + + +# UJSON + + +def _make_ujson_app() -> FastAPI: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=UJSONResponse) + + @app.get("/items") + def get_items() -> Item: + return Item(name="widget", price=9.99) + + return app + + +def test_ujson_response_returns_correct_data(): + app = _make_ujson_app() + client = TestClient(app) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + response = client.get("/items") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + + +def test_ujson_response_emits_deprecation_warning(): + with pytest.warns(FastAPIDeprecationWarning, match="UJSONResponse is deprecated"): + UJSONResponse(content={"hello": "world"}) diff --git a/tests/test_orjson_response_class.py b/tests/test_orjson_response_class.py index 6fe62daf97..63ea054d1f 100644 --- a/tests/test_orjson_response_class.py +++ b/tests/test_orjson_response_class.py @@ -1,9 +1,14 @@ +import warnings + from fastapi import FastAPI +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.responses import ORJSONResponse from fastapi.testclient import TestClient from sqlalchemy.sql.elements import quoted_name -app = FastAPI(default_response_class=ORJSONResponse) +with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=ORJSONResponse) @app.get("/orjson_non_str_keys") @@ -16,6 +21,8 @@ client = TestClient(app) def test_orjson_non_str_keys(): - with client: - response = client.get("/orjson_non_str_keys") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + with client: + response = client.get("/orjson_non_str_keys") assert response.json() == {"msg": "Hello World", "1": 1} diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index cec5ebe6cb..a691dd3a84 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -17,12 +17,14 @@ def get_client(request: pytest.FixtureRequest): return client +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_get_custom_response(client: TestClient): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index 32437db86b..11ce813b76 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -1,17 +1,25 @@ +import warnings + +import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from inline_snapshot import snapshot -from docs_src.custom_response.tutorial001b_py310 import app +with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + from docs_src.custom_response.tutorial001b_py310 import app client = TestClient(app) +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/uv.lock b/uv.lock index 15ca8714f6..0d16c930b1 100644 --- a/uv.lock +++ b/uv.lock @@ -1083,12 +1083,10 @@ all = [ { name = "httpx" }, { name = "itsdangerous" }, { name = "jinja2" }, - { name = "orjson" }, { name = "pydantic-extra-types" }, { name = "pydantic-settings" }, { name = "python-multipart" }, { name = "pyyaml" }, - { name = "ujson" }, { name = "uvicorn", extra = ["standard"] }, ] standard = [ @@ -1134,6 +1132,7 @@ dev = [ { name = "mkdocs-redirects" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, + { name = "orjson" }, { name = "pillow" }, { name = "playwright" }, { name = "prek" }, @@ -1151,6 +1150,7 @@ dev = [ { name = "typer" }, { name = "types-orjson" }, { name = "types-ujson" }, + { name = "ujson" }, ] docs = [ { name = "black" }, @@ -1165,15 +1165,19 @@ docs = [ { name = "mkdocs-material" }, { name = "mkdocs-redirects" }, { name = "mkdocstrings", extra = ["python"] }, + { name = "orjson" }, { name = "pillow" }, { name = "python-slugify" }, { name = "pyyaml" }, { name = "ruff" }, { name = "typer" }, + { name = "ujson" }, ] docs-tests = [ { name = "httpx" }, + { name = "orjson" }, { name = "ruff" }, + { name = "ujson" }, ] github-actions = [ { name = "httpx" }, @@ -1192,6 +1196,7 @@ tests = [ { name = "httpx" }, { name = "inline-snapshot" }, { name = "mypy" }, + { name = "orjson" }, { name = "pwdlib", extra = ["argon2"] }, { name = "pyjwt" }, { name = "pytest" }, @@ -1202,6 +1207,7 @@ tests = [ { name = "strawberry-graphql" }, { name = "types-orjson" }, { name = "types-ujson" }, + { name = "ujson" }, ] translations = [ { name = "gitpython" }, @@ -1225,7 +1231,6 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" }, - { name = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" }, { name = "pydantic", specifier = ">=2.7.0" }, { name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" }, { name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" }, @@ -1240,7 +1245,6 @@ requires-dist = [ { name = "starlette", specifier = ">=0.40.0,<1.0.0" }, { name = "typing-extensions", specifier = ">=4.8.0" }, { name = "typing-inspection", specifier = ">=0.4.2" }, - { name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" }, @@ -1269,6 +1273,7 @@ dev = [ { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, { name = "mypy", specifier = ">=1.14.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "playwright", specifier = ">=1.57.0" }, { name = "prek", specifier = ">=0.2.22" }, @@ -1286,6 +1291,7 @@ dev = [ { name = "typer", specifier = ">=0.21.1" }, { name = "types-orjson", specifier = ">=3.6.2" }, { name = "types-ujson", specifier = ">=5.10.0.20240515" }, + { name = "ujson", specifier = ">=5.8.0" }, ] docs = [ { name = "black", specifier = ">=25.1.0" }, @@ -1300,15 +1306,19 @@ docs = [ { name = "mkdocs-material", specifier = ">=9.7.0" }, { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "python-slugify", specifier = ">=8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "ruff", specifier = ">=0.14.14" }, { name = "typer", specifier = ">=0.21.1" }, + { name = "ujson", specifier = ">=5.8.0" }, ] docs-tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "ruff", specifier = ">=0.14.14" }, + { name = "ujson", specifier = ">=5.8.0" }, ] github-actions = [ { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, @@ -1327,6 +1337,7 @@ tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "inline-snapshot", specifier = ">=0.21.1" }, { name = "mypy", specifier = ">=1.14.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, { name = "pyjwt", specifier = ">=2.9.0" }, { name = "pytest", specifier = ">=9.0.0" }, @@ -1337,6 +1348,7 @@ tests = [ { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, { name = "types-orjson", specifier = ">=3.6.2" }, { name = "types-ujson", specifier = ">=5.10.0.20240515" }, + { name = "ujson", specifier = ">=5.8.0" }, ] translations = [ { name = "gitpython", specifier = ">=3.1.46" }, From 70e8558352ef5f60dbd95c95725a427e06e86a24 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Feb 2026 16:35:25 +0000 Subject: [PATCH 24/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4b7fba3368..b9848e4607 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Breaking Changes + +* 🗑️ Deprecate `ORJSONResponse` and `UJSONResponse`. PR [#14964](https://github.com/fastapi/fastapi/pull/14964) by [@tiangolo](https://github.com/tiangolo). + ## 0.130.0 ### Features From b423b73c355bc401cb49f43cb5ca14515ac088cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 22 Feb 2026 17:36:21 +0100 Subject: [PATCH 25/43] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.131.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b9848e4607..c42133a201 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.131.0 + ### Breaking Changes * 🗑️ Deprecate `ORJSONResponse` and `UJSONResponse`. PR [#14964](https://github.com/fastapi/fastapi/pull/14964) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index ffa56faaf1..944fdd58f4 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.130.0" +__version__ = "0.131.0" from starlette import status as status From 1cea8f659c5ca6ffef1a62b3ac78b9d7c63c3c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 22 Feb 2026 10:13:49 -0800 Subject: [PATCH 26/43] =?UTF-8?q?=F0=9F=91=B7=20Do=20not=20include=20bench?= =?UTF-8?q?mark=20tests=20in=20coverage=20to=20speed=20up=20coverage=20pro?= =?UTF-8?q?cessing=20(#14965)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 49 ++++++++++++++++++++++++++++---------- pyproject.toml | 1 + 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d61b4add94..0d3515efb7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,10 +68,8 @@ jobs: python-version: "3.13" coverage: coverage uv-resolution: highest - # Ubuntu with 3.13 needs coverage for CodSpeed benchmarks - os: ubuntu-latest python-version: "3.13" - coverage: coverage uv-resolution: highest codspeed: codspeed - os: ubuntu-latest @@ -109,20 +107,10 @@ jobs: run: uv pip install "git+https://github.com/Kludex/starlette@main" - run: mkdir coverage - name: Test - if: matrix.codspeed != 'codspeed' run: uv run --no-sync bash scripts/test.sh env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - - name: CodSpeed benchmarks - if: matrix.codspeed == 'codspeed' - uses: CodSpeedHQ/action@v4 - env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} - CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - with: - mode: simulation - run: uv run --no-sync coverage run -m pytest tests/ --codspeed # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow - name: Store coverage files if: matrix.coverage == 'coverage' @@ -132,6 +120,42 @@ jobs: path: coverage include-hidden-files: true + benchmark: + needs: + - changes + if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + env: + UV_PYTHON: "3.13" + UV_RESOLUTION: highest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Install Dependencies + run: uv sync --no-dev --group tests --extra all + - name: CodSpeed benchmarks + uses: CodSpeedHQ/action@v4 + env: + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py3.13 + CONTEXT: ${{ runner.os }}-py3.13 + with: + mode: simulation + run: uv run --no-sync coverage run -m pytest tests/ --codspeed + coverage-combine: needs: - test @@ -176,6 +200,7 @@ jobs: if: always() needs: - coverage-combine + - benchmark runs-on: ubuntu-latest steps: - name: Dump GitHub context diff --git a/pyproject.toml b/pyproject.toml index 79dfc1fd35..76b53726ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -242,6 +242,7 @@ relative_files = true context = '${CONTEXT}' dynamic_context = "test_function" omit = [ + "tests/benchmarks/*", "docs_src/response_model/tutorial003_04_py39.py", "docs_src/response_model/tutorial003_04_py310.py", "docs_src/dependencies/tutorial013_an_py310.py", # temporary code example? From c5559a66dd76c229997c673dd48bbf4f46a2cf68 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Feb 2026 18:14:11 +0000 Subject: [PATCH 27/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c42133a201..7ac314280e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 👷 Do not include benchmark tests in coverage to speed up coverage processing. PR [#14965](https://github.com/fastapi/fastapi/pull/14965) by [@tiangolo](https://github.com/tiangolo). + ## 0.131.0 ### Breaking Changes From 4da264f0f32a1b0eb1c260997d0ab3453f4fdb98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 22 Feb 2026 10:21:38 -0800 Subject: [PATCH 28/43] =?UTF-8?q?=F0=9F=91=B7=20Do=20not=20run=20codspeed?= =?UTF-8?q?=20with=20coverage=20as=20it's=20not=20tracked=20(#14966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d3515efb7..86e6bfc148 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -149,12 +149,9 @@ jobs: run: uv sync --no-dev --group tests --extra all - name: CodSpeed benchmarks uses: CodSpeedHQ/action@v4 - env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py3.13 - CONTEXT: ${{ runner.os }}-py3.13 with: mode: simulation - run: uv run --no-sync coverage run -m pytest tests/ --codspeed + run: uv run --no-sync pytest tests/benchmarks --codspeed coverage-combine: needs: From 282612437835fd1abbc37cf77c6f4e3bc76de557 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Feb 2026 18:22:03 +0000 Subject: [PATCH 29/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7ac314280e..f0fb43062b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Do not run codspeed with coverage as it's not tracked. PR [#14966](https://github.com/fastapi/fastapi/pull/14966) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not include benchmark tests in coverage to speed up coverage processing. PR [#14965](https://github.com/fastapi/fastapi/pull/14965) by [@tiangolo](https://github.com/tiangolo). ## 0.131.0 From a3c8c37272e456b331464b33603788ce1d244c94 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:44:47 +0100 Subject: [PATCH 30/43] =?UTF-8?q?=F0=9F=94=A8=20Fix=20`FastAPI=20People`?= =?UTF-8?q?=20workflow=20(#14951)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/people.py | 83 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/scripts/people.py b/scripts/people.py index f3254ab606..2e84fcc455 100644 --- a/scripts/people.py +++ b/scripts/people.py @@ -5,6 +5,7 @@ import time from collections import Counter from collections.abc import Container from datetime import datetime, timedelta, timezone +from math import ceil from pathlib import Path from typing import Any @@ -15,12 +16,63 @@ from pydantic import BaseModel, SecretStr from pydantic_settings import BaseSettings github_graphql_url = "https://api.github.com/graphql" -questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" +questions_category_id = "DIC_kwDOCZduT84B6E2a" + + +POINTS_PER_MINUTE_LIMIT = 84 # 5000 points per hour + + +class RateLimiter: + def __init__(self) -> None: + self.last_query_cost: int = 1 + self.remaining_points: int = 5000 + self.reset_at: datetime = datetime.fromtimestamp(0, timezone.utc) + self.last_request_start_time: datetime = datetime.fromtimestamp(0, timezone.utc) + self.speed_multiplier: float = 1.0 + + def __enter__(self) -> "RateLimiter": + now = datetime.now(tz=timezone.utc) + + # Handle primary rate limits + primary_limit_wait_time = 0.0 + if self.remaining_points <= self.last_query_cost: + primary_limit_wait_time = (self.reset_at - now).total_seconds() + 2 + logging.warning( + f"Approaching GitHub API rate limit, remaining points: {self.remaining_points}, " + f"reset time in {primary_limit_wait_time} seconds" + ) + + # Handle secondary rate limits + secondary_limit_wait_time = 0.0 + points_per_minute = POINTS_PER_MINUTE_LIMIT * self.speed_multiplier + interval = 60 / (points_per_minute / self.last_query_cost) + time_since_last_request = (now - self.last_request_start_time).total_seconds() + if time_since_last_request < interval: + secondary_limit_wait_time = interval - time_since_last_request + + final_wait_time = ceil(max(primary_limit_wait_time, secondary_limit_wait_time)) + logging.info(f"Sleeping for {final_wait_time} seconds to respect rate limit") + time.sleep(max(final_wait_time, 1)) + + self.last_request_start_time = datetime.now(tz=timezone.utc) + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + pass + + def update_request_info(self, cost: int, remaining: int, reset_at: str) -> None: + self.last_query_cost = cost + self.remaining_points = remaining + self.reset_at = datetime.fromisoformat(reset_at.replace("Z", "+00:00")) + + +rate_limiter = RateLimiter() + discussions_query = """ query Q($after: String, $category_id: ID) { repository(name: "fastapi", owner: "fastapi") { - discussions(first: 100, after: $after, categoryId: $category_id) { + discussions(first: 30, after: $after, categoryId: $category_id) { edges { cursor node { @@ -58,6 +110,11 @@ query Q($after: String, $category_id: ID) { } } } + rateLimit { + cost + remaining + resetAt + } } """ @@ -120,7 +177,7 @@ class Settings(BaseSettings): github_token: SecretStr github_repository: str httpx_timeout: int = 30 - sleep_interval: int = 5 + speed_multiplier: float = 1.0 def get_graphql_response( @@ -158,11 +215,18 @@ def get_graphql_question_discussion_edges( settings: Settings, after: str | None = None, ) -> list[DiscussionsEdge]: - data = get_graphql_response( - settings=settings, - query=discussions_query, - after=after, - category_id=questions_category_id, + with rate_limiter: + data = get_graphql_response( + settings=settings, + query=discussions_query, + after=after, + category_id=questions_category_id, + ) + + rate_limiter.update_request_info( + cost=data["data"]["rateLimit"]["cost"], + remaining=data["data"]["rateLimit"]["remaining"], + reset_at=data["data"]["rateLimit"]["resetAt"], ) graphql_response = DiscussionsResponse.model_validate(data) return graphql_response.data.repository.discussions.edges @@ -185,8 +249,6 @@ def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]: for discussion_edge in discussion_edges: discussion_nodes.append(discussion_edge.node) last_edge = discussion_edges[-1] - # Handle GitHub secondary rate limits, requests per minute - time.sleep(settings.sleep_interval) discussion_edges = get_graphql_question_discussion_edges( settings=settings, after=last_edge.cursor ) @@ -318,6 +380,7 @@ def main() -> None: logging.basicConfig(level=logging.INFO) settings = Settings() logging.info(f"Using config: {settings.model_dump_json()}") + rate_limiter.speed_multiplier = settings.speed_multiplier g = Github(settings.github_token.get_secret_value()) repo = g.get_repo(settings.github_repository) From fef2ce70d928d329af2fa14f9eefe16b716c5219 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Feb 2026 11:45:11 +0000 Subject: [PATCH 31/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f0fb43062b..fc3400034f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 🔨 Fix `FastAPI People` workflow. PR [#14951](https://github.com/fastapi/fastapi/pull/14951) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Do not run codspeed with coverage as it's not tracked. PR [#14966](https://github.com/fastapi/fastapi/pull/14966) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not include benchmark tests in coverage to speed up coverage processing. PR [#14965](https://github.com/fastapi/fastapi/pull/14965) by [@tiangolo](https://github.com/tiangolo). From 5161f7b42b131d945e68606cc4ffcd3b49d407a3 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 23 Feb 2026 16:04:24 +0100 Subject: [PATCH 32/43] =?UTF-8?q?=E2=AC=86=20Update=20all=20dependencies?= =?UTF-8?q?=20to=20use=20`griffelib`=20instead=20of=20`griffe`=20(#14973)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update to griffelib * also update pydantic-ai * move griffelib to get better GH diff * restore accidental edit --- uv.lock | 54 +++++++++++++++++++++++++----------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/uv.lock b/uv.lock index 0d16c930b1..0b14fde532 100644 --- a/uv.lock +++ b/uv.lock @@ -1923,40 +1923,36 @@ wheels = [ ] [[package]] -name = "griffe" -version = "1.15.0" +name = "griffelib" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] [[package]] name = "griffe-typingdoc" -version = "0.3.0" +version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/77/d5e5fa0a8391bc2890ae45255847197299739833108dd76ee3c9b2ff0bba/griffe_typingdoc-0.3.0.tar.gz", hash = "sha256:59d9ef98d02caa7aed88d8df1119c9e48c02ed049ea50ce4018ace9331d20f8b", size = 33169, upload-time = "2025-10-23T12:01:39.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/26/28182e0c8055842bf3da774dee1d5b789c0f236c078dcbdca1937b5214dc/griffe_typingdoc-0.3.1.tar.gz", hash = "sha256:2ff4703115cb7f8a65b9fdcdd1f3c3a15f813b6554621b52eaad094c4782ce96", size = 31218, upload-time = "2026-02-21T09:38:54.409Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/af/aa32c13f753e2625ec895b1f56eee3c9380a2088a88a2c028955e223856e/griffe_typingdoc-0.3.0-py3-none-any.whl", hash = "sha256:4f6483fff7733a679d1dce142fb029f314125f3caaf0d620eb82e7390c8564bb", size = 9923, upload-time = "2025-10-23T12:01:37.601Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c4/cf543fbde49e1ae44830ef0840a4d6ee9f4e4f338138a7766d4e37cf6440/griffe_typingdoc-0.3.1-py3-none-any.whl", hash = "sha256:ecbd457ef6883126b8b6023abf12e08c58e1c152238a2f0e2afdd67a64b07021", size = 10092, upload-time = "2026-02-20T14:53:47.84Z" }, ] [[package]] name = "griffe-warnings-deprecated" -version = "1.1.0" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/0e/f034e1714eb2c694d6196c75f77a02f9c69d19f9961c4804a016397bf3e5/griffe_warnings_deprecated-1.1.0.tar.gz", hash = "sha256:7bf21de327d59c66c7ce08d0166aa4292ce0577ff113de5878f428d102b6f7c5", size = 33260, upload-time = "2024-12-10T21:02:18.395Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/9e/fc86f1e9270f143a395a601de81aa42a871722c34d4b3c7763658dc2e04d/griffe_warnings_deprecated-1.1.1.tar.gz", hash = "sha256:9261369bf2acb8b5d24a0dc7895cce788208513d4349031d4ea315b979b2e99f", size = 26262, upload-time = "2026-02-21T09:38:55.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/4c/b7241f03ad1f22ec2eed33b0f90c4f8c949e3395c4b7488670b07225a20b/griffe_warnings_deprecated-1.1.0-py3-none-any.whl", hash = "sha256:e7b0e8bfd6e5add3945d4d9805b2a41c72409e456733965be276d55f01e8a7a2", size = 5854, upload-time = "2024-12-10T21:02:16.96Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3c/c2a9eee79bf2c8002d2fa370534bee93fdca39e8b1fc82e83d552d5d2c07/griffe_warnings_deprecated-1.1.1-py3-none-any.whl", hash = "sha256:4b7d765e82ca9139ed44ffe7bdebed0d3a46ce014ad5a35a2c22e9a16288737a", size = 6565, upload-time = "2026-02-20T15:35:23.577Z" }, ] [[package]] @@ -3003,17 +2999,17 @@ python = [ [[package]] name = "mkdocstrings-python" -version = "2.0.1" +version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] [[package]] @@ -3937,33 +3933,33 @@ email = [ [[package]] name = "pydantic-ai" -version = "1.56.0" +version = "1.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/1a/800a1e02b259152a49d4c11d9103784a7482c7e9b067eeea23e949d3d80f/pydantic_ai-1.56.0.tar.gz", hash = "sha256:643ff71612df52315b3b4c4b41543657f603f567223eb33245dc8098f005bdc4", size = 11795, upload-time = "2026-02-06T01:13:21.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/97/e3158fa976a29e9580ba1c59601590424bbb81179c359fd29de0dc23aa09/pydantic_ai-1.62.0.tar.gz", hash = "sha256:d6ae517e365ea3ea162ca8ae643f319e105b71b0b6218b83dcad1d1eb2e38c9b", size = 12130, upload-time = "2026-02-19T05:07:07.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/35/f4a7fd2b9962ddb9b021f76f293e74fda71da190bb74b57ed5b343c93022/pydantic_ai-1.56.0-py3-none-any.whl", hash = "sha256:b6b3ac74bdc004693834750da4420ea2cde0d3cbc3f134c0b7544f98f1c00859", size = 7222, upload-time = "2026-02-06T01:13:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7a/053aebfab576603e95fcfce1139de4a87e12bd5a2ef1ba00007a931c3ff0/pydantic_ai-1.62.0-py3-none-any.whl", hash = "sha256:1eb88f745ae045e63da41ad68966e8876c964d0f023fbf5d6a3f5d243370bd04", size = 7227, upload-time = "2026-02-19T05:06:58.341Z" }, ] [[package]] name = "pydantic-ai-slim" -version = "1.56.0" +version = "1.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "genai-prices" }, - { name = "griffe" }, + { name = "griffelib" }, { name = "httpx" }, { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/3a577825b9c1da8f287be7f2ee6fe9aab48bc8a80e65c8518052c589f51c/pydantic_ai_slim-1.56.0.tar.gz", hash = "sha256:9f9f9c56b1c735837880a515ae5661b465b40207b25f3a3434178098b2137f05", size = 415265, upload-time = "2026-02-06T01:13:23.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8d/6350a49f2e4b636efbcfc233221420ab576e4ba4edba38254cb84ae4a1e6/pydantic_ai_slim-1.62.0.tar.gz", hash = "sha256:00d84f659107bbbd88823a3d3dbe7348385935a9870b9d7d4ba799256f6b6983", size = 422452, upload-time = "2026-02-19T05:07:10.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/4b/34682036528eeb9aaf093c2073540ddf399ab37b99d282a69ca41356f1aa/pydantic_ai_slim-1.56.0-py3-none-any.whl", hash = "sha256:d657e4113485020500b23b7390b0066e2a0277edc7577eaad2290735ca5dd7d5", size = 542270, upload-time = "2026-02-06T01:13:14.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/67/21e9b3b0944568662e3790c936226bd48a9f27c6b5f27b5916f5857bc4d8/pydantic_ai_slim-1.62.0-py3-none-any.whl", hash = "sha256:5210073fadd46f65859a67da67845093c487f025fa430ed027151f22ec684ab2", size = 549296, upload-time = "2026-02-19T05:07:01.624Z" }, ] [package.optional-dependencies] @@ -4181,7 +4177,7 @@ wheels = [ [[package]] name = "pydantic-graph" -version = "1.56.0" +version = "1.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -4189,9 +4185,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/03/f92881cdb12d6f43e60e9bfd602e41c95408f06e2324d3729f7a194e2bcd/pydantic_graph-1.56.0.tar.gz", hash = "sha256:5e22972dbb43dbc379ab9944252ff864019abf3c7d465dcdf572fc8aec9a44a1", size = 58460, upload-time = "2026-02-06T01:13:26.708Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/b6/0b084c847ecd99624f4fbc5c8ecd3f67a2388a282a32612b2a68c3b3595f/pydantic_graph-1.62.0.tar.gz", hash = "sha256:efe56bee3a8ca35b11a3be6a5f7352419fe182ef1e1323a3267ee12dec95f3c7", size = 58529, upload-time = "2026-02-19T05:07:12.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/07/8c823eb4d196137c123d4d67434e185901d3cbaea3b0c2b7667da84e72c1/pydantic_graph-1.56.0-py3-none-any.whl", hash = "sha256:ec3f0a1d6fcedd4eb9c59fef45079c2ee4d4185878d70dae26440a9c974c6bb3", size = 72346, upload-time = "2026-02-06T01:13:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/f0/12/1a9cbcd59fd070ba72b0fe544caa6ca97758518643523ec2bf1162084e0d/pydantic_graph-1.62.0-py3-none-any.whl", hash = "sha256:abe0e7b356b4d3202b069ec020d8dd1f647f55e9a0e85cd272dab48250bde87d", size = 72350, upload-time = "2026-02-19T05:07:05.305Z" }, ] [[package]] From da1937443d80fa232ba3a401ddf9117e3cb43555 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Feb 2026 15:04:55 +0000 Subject: [PATCH 33/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fc3400034f..b423686147 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ Update all dependencies to use `griffelib` instead of `griffe`. PR [#14973](https://github.com/fastapi/fastapi/pull/14973) by [@svlandeg](https://github.com/svlandeg). * 🔨 Fix `FastAPI People` workflow. PR [#14951](https://github.com/fastapi/fastapi/pull/14951) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Do not run codspeed with coverage as it's not tracked. PR [#14966](https://github.com/fastapi/fastapi/pull/14966) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not include benchmark tests in coverage to speed up coverage processing. PR [#14965](https://github.com/fastapi/fastapi/pull/14965) by [@tiangolo](https://github.com/tiangolo). From 248d7fb9f5d3d0ac8202f7a03546441cd0f53c1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:49:53 +0100 Subject: [PATCH 34/43] =?UTF-8?q?=E2=AC=86=20Bump=20flask=20from=203.1.2?= =?UTF-8?q?=20to=203.1.3=20(#14949)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [flask](https://github.com/pallets/flask) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3) --- updated-dependencies: - dependency-name: flask dependency-version: 3.1.3 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 0b14fde532..6faba96621 100644 --- a/uv.lock +++ b/uv.lock @@ -1607,7 +1607,7 @@ wheels = [ [[package]] name = "flask" -version = "3.1.2" +version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, @@ -1617,9 +1617,9 @@ dependencies = [ { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] From 94a1ee749e63591e6e275b726a66e07a25f6c407 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Feb 2026 16:50:41 +0000 Subject: [PATCH 35/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b423686147..51d82313fd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ Bump flask from 3.1.2 to 3.1.3. PR [#14949](https://github.com/fastapi/fastapi/pull/14949) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update all dependencies to use `griffelib` instead of `griffe`. PR [#14973](https://github.com/fastapi/fastapi/pull/14973) by [@svlandeg](https://github.com/svlandeg). * 🔨 Fix `FastAPI People` workflow. PR [#14951](https://github.com/fastapi/fastapi/pull/14951) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Do not run codspeed with coverage as it's not tracked. PR [#14966](https://github.com/fastapi/fastapi/pull/14966) by [@tiangolo](https://github.com/tiangolo). From 22354a253037e0fb23e55dabcb8767943e371702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Feb 2026 09:45:20 -0800 Subject: [PATCH 36/43] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Add=20`strict=5Fc?= =?UTF-8?q?ontent=5Ftype`=20checking=20for=20JSON=20requests=20(#14978)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/strict-content-type.md | 88 ++++++++++++++++++ docs/en/mkdocs.yml | 1 + docs_src/strict_content_type/__init__.py | 0 .../strict_content_type/tutorial001_py310.py | 14 +++ fastapi/applications.py | 24 +++++ fastapi/routing.py | 44 ++++++++- tests/test_strict_content_type_app_level.py | 44 +++++++++ tests/test_strict_content_type_nested.py | 91 +++++++++++++++++++ .../test_strict_content_type_router_level.py | 61 +++++++++++++ .../test_body/test_tutorial001.py | 10 +- .../test_strict_content_type/__init__.py | 0 .../test_tutorial001.py | 43 +++++++++ 12 files changed, 411 insertions(+), 9 deletions(-) create mode 100644 docs/en/docs/advanced/strict-content-type.md create mode 100644 docs_src/strict_content_type/__init__.py create mode 100644 docs_src/strict_content_type/tutorial001_py310.py create mode 100644 tests/test_strict_content_type_app_level.py create mode 100644 tests/test_strict_content_type_nested.py create mode 100644 tests/test_strict_content_type_router_level.py create mode 100644 tests/test_tutorial/test_strict_content_type/__init__.py create mode 100644 tests/test_tutorial/test_strict_content_type/test_tutorial001.py diff --git a/docs/en/docs/advanced/strict-content-type.md b/docs/en/docs/advanced/strict-content-type.md new file mode 100644 index 0000000000..54c099410c --- /dev/null +++ b/docs/en/docs/advanced/strict-content-type.md @@ -0,0 +1,88 @@ +# Strict Content-Type Checking { #strict-content-type-checking } + +By default, **FastAPI** uses strict `Content-Type` header checking for JSON request bodies, this means that JSON requests **must** include a valid `Content-Type` header (e.g. `application/json`) in order for the body to be parsed as JSON. + +## CSRF Risk { #csrf-risk } + +This default behavior provides protection against a class of **Cross-Site Request Forgery (CSRF)** attacks in a very specific scenario. + +These attacks exploit the fact that browsers allow scripts to send requests without doing any CORS preflight check when they: + +* don't have a `Content-Type` header (e.g. using `fetch()` with a `Blob` body) +* and don't send any authentication credentials. + +This type of attack is mainly relevant when: + +* the application is running locally (e.g. on `localhost`) or in an internal network +* and the application doesn't have any authentication, it expects that any request from the same network can be trusted. + +## Example Attack { #example-attack } + +Imagine you build a way to run a local AI agent. + +It provides an API at + +``` +http://localhost:8000/v1/agents/multivac +``` + +There's also a frontend at + +``` +http://localhost:8000 +``` + +/// tip + +Note that both have the same host. + +/// + +Then using the frontend you can make the AI agent do things on your behalf. + +As it's running **locally**, and not in the open internet, you decide to **not have any authentication** set up, just trusting the access to the local network. + +Then one of your users could install it and run it locally. + +Then they could open a malicious website, e.g. something like + +``` +https://evilhackers.example.com +``` + +And that malicious website sends requests using `fetch()` with a `Blob` body to the local API at + +``` +http://localhost:8000/v1/agents/multivac +``` + +Even though the host of the malicious website and the local app is different, the browser won't trigger a CORS preflight request because: + +* It's running without any authentication, it doesn't have to send any credentials. +* The browser thinks it's not sending JSON (because of the missing `Content-Type` header). + +Then the malicious website could make the local AI agent send angry messages to the user's ex-boss... or worse. 😅 + +## Open Internet { #open-internet } + +If your app is in the open internet, you wouldn't "trust the network" and let anyone send privileged requests without authentication. + +Attackers could simply run a script to send requests to your API, no need for browser interaction, so you are probably already securing any privileged endpoints. + +In that case **this attack / risk doesn't apply to you**. + +This risk and attack is mainly relevant when the app runs on the **local network** and that is the **only assumed protection**. + +## Allowing Requests Without Content-Type { #allowing-requests-without-content-type } + +If you need to support clients that don't send a `Content-Type` header, you can disable strict checking by setting `strict_content_type=False`: + +{* ../../docs_src/strict_content_type/tutorial001_py310.py hl[4] *} + +With this setting, requests without a `Content-Type` header will have their body parsed as JSON, which is the same behavior as older versions of FastAPI. + +/// info + +This behavior and configuration was added in FastAPI 0.132.0. + +/// diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index b276e55d95..e86e7b9c41 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -193,6 +193,7 @@ nav: - advanced/generate-clients.md - advanced/advanced-python-types.md - advanced/json-base64-bytes.md + - advanced/strict-content-type.md - fastapi-cli.md - Deployment: - deployment/index.md diff --git a/docs_src/strict_content_type/__init__.py b/docs_src/strict_content_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/strict_content_type/tutorial001_py310.py b/docs_src/strict_content_type/tutorial001_py310.py new file mode 100644 index 0000000000..a44f4b1386 --- /dev/null +++ b/docs_src/strict_content_type/tutorial001_py310.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI(strict_content_type=False) + + +class Item(BaseModel): + name: str + price: float + + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/fastapi/applications.py b/fastapi/applications.py index 41d86143ec..ed05a1ff9e 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -840,6 +840,29 @@ class FastAPI(Starlette): """ ), ] = None, + strict_content_type: Annotated[ + bool, + Doc( + """ + Enable strict checking for request Content-Type headers. + + When `True` (the default), requests with a body that do not include + a `Content-Type` header will **not** be parsed as JSON. + + This prevents potential cross-site request forgery (CSRF) attacks + that exploit the browser's ability to send requests without a + Content-Type header, bypassing CORS preflight checks. In particular + applicable for apps that need to be run locally (in localhost). + + When `False`, requests without a `Content-Type` header will have + their body parsed as JSON, which maintains compatibility with + certain clients that don't send `Content-Type` headers. + + Read more about it in the + [FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/). + """ + ), + ] = True, **extra: Annotated[ Any, Doc( @@ -974,6 +997,7 @@ class FastAPI(Starlette): include_in_schema=include_in_schema, responses=responses, generate_unique_id_function=generate_unique_id_function, + strict_content_type=strict_content_type, ) self.exception_handlers: dict[ Any, Callable[[Request, Any], Response | Awaitable[Response]] diff --git a/fastapi/routing.py b/fastapi/routing.py index 528c962965..d17650a627 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -329,6 +329,7 @@ def get_request_handler( response_model_exclude_none: bool = False, dependency_overrides_provider: Any | None = None, embed_body_fields: bool = False, + strict_content_type: bool | DefaultPlaceholder = Default(True), ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = dependant.is_coroutine_callable @@ -337,6 +338,10 @@ def get_request_handler( actual_response_class: type[Response] = response_class.value else: actual_response_class = response_class + if isinstance(strict_content_type, DefaultPlaceholder): + actual_strict_content_type: bool = strict_content_type.value + else: + actual_strict_content_type = strict_content_type async def app(request: Request) -> Response: response: Response | None = None @@ -370,7 +375,8 @@ def get_request_handler( json_body: Any = Undefined content_type_value = request.headers.get("content-type") if not content_type_value: - json_body = await request.json() + if not actual_strict_content_type: + json_body = await request.json() else: message = email.message.Message() message["content-type"] = content_type_value @@ -599,6 +605,7 @@ class APIRoute(routing.Route): openapi_extra: dict[str, Any] | None = None, generate_unique_id_function: Callable[["APIRoute"], str] | DefaultPlaceholder = Default(generate_unique_id), + strict_content_type: bool | DefaultPlaceholder = Default(True), ) -> None: self.path = path self.endpoint = endpoint @@ -625,6 +632,7 @@ class APIRoute(routing.Route): self.callbacks = callbacks self.openapi_extra = openapi_extra self.generate_unique_id_function = generate_unique_id_function + self.strict_content_type = strict_content_type self.tags = tags or [] self.responses = responses or {} self.name = get_name(endpoint) if name is None else name @@ -713,6 +721,7 @@ class APIRoute(routing.Route): response_model_exclude_none=self.response_model_exclude_none, dependency_overrides_provider=self.dependency_overrides_provider, embed_body_fields=self._embed_body_fields, + strict_content_type=self.strict_content_type, ) def matches(self, scope: Scope) -> tuple[Match, Scope]: @@ -963,6 +972,29 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + strict_content_type: Annotated[ + bool, + Doc( + """ + Enable strict checking for request Content-Type headers. + + When `True` (the default), requests with a body that do not include + a `Content-Type` header will **not** be parsed as JSON. + + This prevents potential cross-site request forgery (CSRF) attacks + that exploit the browser's ability to send requests without a + Content-Type header, bypassing CORS preflight checks. In particular + applicable for apps that need to be run locally (in localhost). + + When `False`, requests without a `Content-Type` header will have + their body parsed as JSON, which maintains compatibility with + certain clients that don't send `Content-Type` headers. + + Read more about it in the + [FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/). + """ + ), + ] = Default(True), ) -> None: # Determine the lifespan context to use if lifespan is None: @@ -1009,6 +1041,7 @@ class APIRouter(routing.Router): self.route_class = route_class self.default_response_class = default_response_class self.generate_unique_id_function = generate_unique_id_function + self.strict_content_type = strict_content_type def route( self, @@ -1059,6 +1092,7 @@ class APIRouter(routing.Router): openapi_extra: dict[str, Any] | None = None, generate_unique_id_function: Callable[[APIRoute], str] | DefaultPlaceholder = Default(generate_unique_id), + strict_content_type: bool | DefaultPlaceholder = Default(True), ) -> None: route_class = route_class_override or self.route_class responses = responses or {} @@ -1105,6 +1139,9 @@ class APIRouter(routing.Router): callbacks=current_callbacks, openapi_extra=openapi_extra, generate_unique_id_function=current_generate_unique_id, + strict_content_type=get_value_or_default( + strict_content_type, self.strict_content_type + ), ) self.routes.append(route) @@ -1480,6 +1517,11 @@ class APIRouter(routing.Router): callbacks=current_callbacks, openapi_extra=route.openapi_extra, generate_unique_id_function=current_generate_unique_id, + strict_content_type=get_value_or_default( + route.strict_content_type, + router.strict_content_type, + self.strict_content_type, + ), ) elif isinstance(route, routing.Route): methods = list(route.methods or []) diff --git a/tests/test_strict_content_type_app_level.py b/tests/test_strict_content_type_app_level.py new file mode 100644 index 0000000000..42a0821a47 --- /dev/null +++ b/tests/test_strict_content_type_app_level.py @@ -0,0 +1,44 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +app_default = FastAPI() + + +@app_default.post("/items/") +async def app_default_post(data: dict): + return data + + +app_lax = FastAPI(strict_content_type=False) + + +@app_lax.post("/items/") +async def app_lax_post(data: dict): + return data + + +client_default = TestClient(app_default) +client_lax = TestClient(app_lax) + + +def test_default_strict_rejects_no_content_type(): + response = client_default.post("/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_default_strict_accepts_json_content_type(): + response = client_default.post("/items/", json={"key": "value"}) + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_lax_accepts_no_content_type(): + response = client_lax.post("/items/", content='{"key": "value"}') + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_lax_accepts_json_content_type(): + response = client_lax.post("/items/", json={"key": "value"}) + assert response.status_code == 200 + assert response.json() == {"key": "value"} diff --git a/tests/test_strict_content_type_nested.py b/tests/test_strict_content_type_nested.py new file mode 100644 index 0000000000..922d01571a --- /dev/null +++ b/tests/test_strict_content_type_nested.py @@ -0,0 +1,91 @@ +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient + +# Lax app with nested routers, inner overrides to strict + +app_nested = FastAPI(strict_content_type=False) # lax app +outer_router = APIRouter(prefix="/outer") # inherits lax from app +inner_strict = APIRouter(prefix="/strict", strict_content_type=True) +inner_default = APIRouter(prefix="/default") + + +@inner_strict.post("/items/") +async def inner_strict_post(data: dict): + return data + + +@inner_default.post("/items/") +async def inner_default_post(data: dict): + return data + + +outer_router.include_router(inner_strict) +outer_router.include_router(inner_default) +app_nested.include_router(outer_router) + +client_nested = TestClient(app_nested) + + +def test_strict_inner_on_lax_app_rejects_no_content_type(): + response = client_nested.post("/outer/strict/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_default_inner_inherits_lax_from_app(): + response = client_nested.post("/outer/default/items/", content='{"key": "value"}') + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_strict_inner_accepts_json_content_type(): + response = client_nested.post("/outer/strict/items/", json={"key": "value"}) + assert response.status_code == 200 + + +def test_default_inner_accepts_json_content_type(): + response = client_nested.post("/outer/default/items/", json={"key": "value"}) + assert response.status_code == 200 + + +# Strict app -> lax outer router -> strict inner router + +app_mixed = FastAPI(strict_content_type=True) +mixed_outer = APIRouter(prefix="/outer", strict_content_type=False) +mixed_inner = APIRouter(prefix="/inner", strict_content_type=True) + + +@mixed_outer.post("/items/") +async def mixed_outer_post(data: dict): + return data + + +@mixed_inner.post("/items/") +async def mixed_inner_post(data: dict): + return data + + +mixed_outer.include_router(mixed_inner) +app_mixed.include_router(mixed_outer) + +client_mixed = TestClient(app_mixed) + + +def test_lax_outer_on_strict_app_accepts_no_content_type(): + response = client_mixed.post("/outer/items/", content='{"key": "value"}') + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_strict_inner_on_lax_outer_rejects_no_content_type(): + response = client_mixed.post("/outer/inner/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_lax_outer_accepts_json_content_type(): + response = client_mixed.post("/outer/items/", json={"key": "value"}) + assert response.status_code == 200 + + +def test_strict_inner_on_lax_outer_accepts_json_content_type(): + response = client_mixed.post("/outer/inner/items/", json={"key": "value"}) + assert response.status_code == 200 diff --git a/tests/test_strict_content_type_router_level.py b/tests/test_strict_content_type_router_level.py new file mode 100644 index 0000000000..72a02d6c91 --- /dev/null +++ b/tests/test_strict_content_type_router_level.py @@ -0,0 +1,61 @@ +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + +router_lax = APIRouter(prefix="/lax", strict_content_type=False) +router_strict = APIRouter(prefix="/strict", strict_content_type=True) +router_default = APIRouter(prefix="/default") + + +@router_lax.post("/items/") +async def router_lax_post(data: dict): + return data + + +@router_strict.post("/items/") +async def router_strict_post(data: dict): + return data + + +@router_default.post("/items/") +async def router_default_post(data: dict): + return data + + +app.include_router(router_lax) +app.include_router(router_strict) +app.include_router(router_default) + +client = TestClient(app) + + +def test_lax_router_on_strict_app_accepts_no_content_type(): + response = client.post("/lax/items/", content='{"key": "value"}') + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_strict_router_on_strict_app_rejects_no_content_type(): + response = client.post("/strict/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_default_router_inherits_strict_from_app(): + response = client.post("/default/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_lax_router_accepts_json_content_type(): + response = client.post("/lax/items/", json={"key": "value"}) + assert response.status_code == 200 + + +def test_strict_router_accepts_json_content_type(): + response = client.post("/strict/items/", json={"key": "value"}) + assert response.status_code == 200 + + +def test_default_router_accepts_json_content_type(): + response = client.post("/default/items/", json={"key": "value"}) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index bdabf8d68b..8c883708a3 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -189,18 +189,12 @@ def test_geo_json(client: TestClient): assert response.status_code == 200, response.text -def test_no_content_type_is_json(client: TestClient): +def test_no_content_type_json(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', ) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "price": 50.5, - "tax": None, - } + assert response.status_code == 422, response.text def test_wrong_headers(client: TestClient): diff --git a/tests/test_tutorial/test_strict_content_type/__init__.py b/tests/test_tutorial/test_strict_content_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_strict_content_type/test_tutorial001.py b/tests/test_tutorial/test_strict_content_type/test_tutorial001.py new file mode 100644 index 0000000000..81e2d3a0be --- /dev/null +++ b/tests/test_tutorial/test_strict_content_type/test_tutorial001.py @@ -0,0 +1,43 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_py310", + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.strict_content_type.{request.param}") + client = TestClient(mod.app) + return client + + +def test_lax_post_without_content_type_is_parsed_as_json(client: TestClient): + response = client.post( + "/items/", + content='{"name": "Foo", "price": 50.5}', + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 50.5} + + +def test_lax_post_with_json_content_type(client: TestClient): + response = client.post( + "/items/", + json={"name": "Foo", "price": 50.5}, + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 50.5} + + +def test_lax_post_with_text_plain_is_still_rejected(client: TestClient): + response = client.post( + "/items/", + content='{"name": "Foo", "price": 50.5}', + headers={"Content-Type": "text/plain"}, + ) + assert response.status_code == 422, response.text From ac8621a76eba48c29cead5cd0dcdd77d446d37ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Feb 2026 17:46:11 +0000 Subject: [PATCH 37/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 51d82313fd..0be7bbc9c0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Breaking Changes + +* 🔒️ Add `strict_content_type` checking for JSON requests. PR [#14978](https://github.com/fastapi/fastapi/pull/14978) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ⬆ Bump flask from 3.1.2 to 3.1.3. PR [#14949](https://github.com/fastapi/fastapi/pull/14949) by [@dependabot[bot]](https://github.com/apps/dependabot). From 5c863d0718cea2e0ed812fbbfdc5844a0a34b039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Feb 2026 18:46:57 +0100 Subject: [PATCH 38/43] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.132.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0be7bbc9c0..7ea70eceb9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.132.0 + ### Breaking Changes * 🔒️ Add `strict_content_type` checking for JSON requests. PR [#14978](https://github.com/fastapi/fastapi/pull/14978) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 944fdd58f4..d936bb7df3 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.131.0" +__version__ = "0.132.0" from starlette import status as status From acdf52e0c89f81952ab17bb7b34d67deb0d533dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Feb 2026 18:54:18 +0100 Subject: [PATCH 39/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7ea70eceb9..08826adff6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -12,6 +12,9 @@ hide: ### Breaking Changes * 🔒️ Add `strict_content_type` checking for JSON requests. PR [#14978](https://github.com/fastapi/fastapi/pull/14978) by [@tiangolo](https://github.com/tiangolo). + * Now FastAPI checks, by default, that JSON requests have a `Content-Type` header with a valid JSON value, like `application/json`, and rejects requests that don't. + * If the clients for your app don't send a valid `Content-Type` header you can disable this with `strict_content_type=False`. + * Check the new docs: [Strict Content-Type Checking](https://fastapi.tiangolo.com/advanced/strict-content-type/). ### Internal From 6af38321261afde391dd28f60ec0dab9c15f0629 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:31:54 +0100 Subject: [PATCH 40/43] =?UTF-8?q?=F0=9F=91=B7=20Allow=20skipping=20`benchm?= =?UTF-8?q?ark`=20job=20in=20`test`=20workflow=20(#14974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86e6bfc148..6046a4560d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -208,4 +208,4 @@ jobs: uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} - allowed-skips: coverage-combine,test + allowed-skips: coverage-combine,test,benchmark From 3f30ca1a5e2ed4cca1f8102b39a40e56151a018e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Feb 2026 18:32:32 +0000 Subject: [PATCH 41/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 08826adff6..2b068a5727 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* 👷 Allow skipping `benchmark` job in `test` workflow. PR [#14974](https://github.com/fastapi/fastapi/pull/14974) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.132.0 ### Breaking Changes From 0cf27ecf8826ab8d1c8c21a4b9883ddae480d2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Feb 2026 10:47:59 -0800 Subject: [PATCH 42/43] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20People?= =?UTF-8?q?=20-=20Experts=20(#14972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- docs/en/data/people.yml | 932 ++++++++++++++++++++-------------------- 1 file changed, 470 insertions(+), 462 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 2fdb21a059..89269ecd69 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,23 +1,23 @@ maintainers: - login: tiangolo - answers: 1900 + answers: 1923 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo experts: - login: tiangolo - count: 1900 + count: 1923 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo - login: YuriiMotov - count: 971 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + count: 1107 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 url: https://github.com/YuriiMotov - login: github-actions - count: 769 + count: 770 avatarUrl: https://avatars.githubusercontent.com/in/15368?v=4 url: https://github.com/apps/github-actions - login: Kludex - count: 654 + count: 656 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex - login: jgould22 @@ -37,7 +37,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=f1e7bae394a315da950912c92dc861a8eaf95d4c&v=4 url: https://github.com/ycd - login: JarroVGIT - count: 190 + count: 192 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 @@ -53,11 +53,11 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: JavierSanchezCastro - count: 94 + count: 106 avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 url: https://github.com/JavierSanchezCastro - login: luzzodev - count: 89 + count: 104 avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 url: https://github.com/luzzodev - login: raphaelauv @@ -81,32 +81,32 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben - login: yinziyan1206 - count: 54 + count: 55 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 url: https://github.com/yinziyan1206 +- login: acidjunk + count: 50 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: sm-Fifteen count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: acidjunk - count: 49 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: adriangb count: 46 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 url: https://github.com/adriangb -- login: Dustyposa - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 - url: https://github.com/Dustyposa - login: insomnes count: 45 avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 url: https://github.com/insomnes +- login: Dustyposa + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 + url: https://github.com/Dustyposa - login: frankie567 count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=f3e79acfe4ed207e15c2145161a8a9759925fcd2&v=4 url: https://github.com/frankie567 - login: odiseo0 count: 43 @@ -120,14 +120,14 @@ experts: count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin -- login: STeveShary - count: 37 - avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 - url: https://github.com/STeveShary - login: chbndrhnns count: 37 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns +- login: STeveShary + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 @@ -136,18 +136,22 @@ experts: count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla +- login: valentinDruzhinin + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin - login: prostomarkeloff count: 28 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=6918e39a1224194ba636e897461a02a20126d7ad&v=4 url: https://github.com/prostomarkeloff -- login: hasansezertasan - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan - login: alv2017 - count: 26 + count: 27 avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 url: https://github.com/alv2017 +- login: hasansezertasan + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=d36995e41a00590da64e6204cfd112e0484ac1ca&v=4 + url: https://github.com/hasansezertasan - login: dbanty count: 26 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9d726785d08e50b1e1cd96505800c8ea8405bce2&v=4 @@ -156,10 +160,6 @@ experts: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: valentinDruzhinin - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin - login: SirTelemak count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 @@ -176,6 +176,10 @@ experts: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 url: https://github.com/chrisK824 +- login: ceb10n + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n - login: rafsaf count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 @@ -194,7 +198,7 @@ experts: url: https://github.com/ebottos94 - login: estebanx64 count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=1900887aeed268699e5ea6f3fb7db614f7b77cd3&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=2ca073ee47a625e495a9573bd374ddcd7be5ec91&v=4 url: https://github.com/estebanx64 - login: sehraramiz count: 18 @@ -236,467 +240,471 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=f601c3f111f2148bd9244c2cb3ebbd57b592e674&v=4 url: https://github.com/jonatasoli -- login: ghost - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 - url: https://github.com/ghost -- login: abhint - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 - url: https://github.com/abhint -last_month_experts: -- login: YuriiMotov - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov -- login: valentinDruzhinin - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin -- login: yinziyan1206 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: tiangolo - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo -- login: luzzodev - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 - url: https://github.com/luzzodev -three_months_experts: -- login: YuriiMotov - count: 397 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov -- login: valentinDruzhinin - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin -- login: luzzodev - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 - url: https://github.com/luzzodev -- login: raceychan - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/75417963?u=060c62870ec5a791765e63ac20d8885d11143786&v=4 - url: https://github.com/raceychan -- login: yinziyan1206 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: DoctorJohn - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14076775?u=2913e70a6142772847e91e2aaa5b9152391715e9&v=4 - url: https://github.com/DoctorJohn -- login: tiangolo - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo -- login: sachinh35 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 - url: https://github.com/sachinh35 -- login: eqsdxr - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=58fddf77ed76966eaa8c73eea9bea4bb0c53b673&v=4 - url: https://github.com/eqsdxr -- login: Jelle-tenB - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/210023470?u=c25d66addf36a747bd9fab773c4a6e7b238f45d4&v=4 - url: https://github.com/Jelle-tenB -- login: pythonweb2 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: WilliamDEdwards - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 - url: https://github.com/WilliamDEdwards -- login: Brikas - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/80290187?v=4 - url: https://github.com/Brikas -- login: purepani - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/7587353?v=4 - url: https://github.com/purepani -- login: JavierSanchezCastro - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: TaigoFr - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17792131?u=372b27056ec82f1ae03d8b3f37ef55b04a7cfdd1&v=4 - url: https://github.com/TaigoFr -- login: Garrett-R - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/6614695?u=c128fd775002882f6e391bda5a89d1bdc5bdf45f&v=4 - url: https://github.com/Garrett-R -- login: jymchng - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/27895426?u=fb88c47775147d62a395fdb895d1af4148c7b566&v=4 - url: https://github.com/jymchng -- login: davidhuser - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4357648?u=6ed702f8f6d49a8b2a0ed33cbd8ab59c2d7db7f7&v=4 - url: https://github.com/davidhuser -six_months_experts: -- login: YuriiMotov - count: 763 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov -- login: luzzodev - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 - url: https://github.com/luzzodev -- login: valentinDruzhinin - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin -- login: alv2017 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 - url: https://github.com/alv2017 -- login: sachinh35 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 - url: https://github.com/sachinh35 -- login: yauhen-sobaleu - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/51629535?u=fc1817060daf2df438bfca86c44f33da5cd667db&v=4 - url: https://github.com/yauhen-sobaleu -- login: tiangolo - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo -- login: JavierSanchezCastro - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: raceychan - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/75417963?u=060c62870ec5a791765e63ac20d8885d11143786&v=4 - url: https://github.com/raceychan -- login: yinziyan1206 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: DoctorJohn - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14076775?u=2913e70a6142772847e91e2aaa5b9152391715e9&v=4 - url: https://github.com/DoctorJohn -- login: eqsdxr - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=58fddf77ed76966eaa8c73eea9bea4bb0c53b673&v=4 - url: https://github.com/eqsdxr -- login: Kludex - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 - url: https://github.com/Kludex -- login: Jelle-tenB - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/210023470?u=c25d66addf36a747bd9fab773c4a6e7b238f45d4&v=4 - url: https://github.com/Jelle-tenB -- login: adsouza - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/275832?u=f90f110cfafeafed2f14339e840941c2c328c186&v=4 - url: https://github.com/adsouza -- login: pythonweb2 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: WilliamDEdwards - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 - url: https://github.com/WilliamDEdwards -- login: Brikas - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/80290187?v=4 - url: https://github.com/Brikas -- login: purepani - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/7587353?v=4 - url: https://github.com/purepani -- login: TaigoFr - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17792131?u=372b27056ec82f1ae03d8b3f37ef55b04a7cfdd1&v=4 - url: https://github.com/TaigoFr -- login: Garrett-R - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/6614695?u=c128fd775002882f6e391bda5a89d1bdc5bdf45f&v=4 - url: https://github.com/Garrett-R -- login: EverStarck - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/51029456?u=343409b7cb6b3ea6a59359f4e8370d9c3f140ecd&v=4 - url: https://github.com/EverStarck -- login: henrymcl - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/26480299?v=4 - url: https://github.com/henrymcl -- login: jymchng - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/27895426?u=fb88c47775147d62a395fdb895d1af4148c7b566&v=4 - url: https://github.com/jymchng -- login: davidhuser - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4357648?u=6ed702f8f6d49a8b2a0ed33cbd8ab59c2d7db7f7&v=4 - url: https://github.com/davidhuser -- login: PidgeyBE - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/19860056?u=47b584eb1c1ab45e31c1b474109a962d7e82be49&v=4 - url: https://github.com/PidgeyBE -- login: KianAnbarestani - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/145364424?u=dcc3d8fb4ca07d36fb52a17f38b6650565de40be&v=4 - url: https://github.com/KianAnbarestani -- login: jgould22 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: marsboy02 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/86903678?u=04cc319d6605f8d1ba3a0bed9f4f55a582719ae6&v=4 - url: https://github.com/marsboy02 -one_year_experts: -- login: YuriiMotov - count: 824 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov -- login: luzzodev - count: 89 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 - url: https://github.com/luzzodev -- login: Kludex - count: 50 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 - url: https://github.com/Kludex -- login: sinisaos - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 - url: https://github.com/sinisaos -- login: alv2017 - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 - url: https://github.com/alv2017 -- login: valentinDruzhinin - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin -- login: JavierSanchezCastro - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: jgould22 - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: tiangolo - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo -- login: Kfir-G - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=a3bf923ab27bce3d1b13779a8dd22eb7675017fd&v=4 - url: https://github.com/Kfir-G -- login: sehraramiz - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 - url: https://github.com/sehraramiz -- login: sachinh35 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 - url: https://github.com/sachinh35 -- login: yauhen-sobaleu - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/51629535?u=fc1817060daf2df438bfca86c44f33da5cd667db&v=4 - url: https://github.com/yauhen-sobaleu -- login: estebanx64 - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=1900887aeed268699e5ea6f3fb7db614f7b77cd3&v=4 - url: https://github.com/estebanx64 -- login: ceb10n - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n -- login: yvallois - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 - url: https://github.com/yvallois -- login: raceychan - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/75417963?u=060c62870ec5a791765e63ac20d8885d11143786&v=4 - url: https://github.com/raceychan -- login: yinziyan1206 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: DoctorJohn - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14076775?u=2913e70a6142772847e91e2aaa5b9152391715e9&v=4 - url: https://github.com/DoctorJohn -- login: n8sty - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: pythonweb2 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: eqsdxr - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=58fddf77ed76966eaa8c73eea9bea4bb0c53b673&v=4 - url: https://github.com/eqsdxr -- login: yokwejuste - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=4ba43bd63c169b5c015137d8916752a44001445a&v=4 - url: https://github.com/yokwejuste -- login: WilliamDEdwards - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 - url: https://github.com/WilliamDEdwards - login: mattmess1221 - count: 3 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=d22ea18aa8ea688af25a45df306134d593621a44&v=4 url: https://github.com/mattmess1221 -- login: Jelle-tenB - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/210023470?u=c25d66addf36a747bd9fab773c4a6e7b238f45d4&v=4 - url: https://github.com/Jelle-tenB -- login: viniciusCalcantara - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=80f3ec7427fa6a41d5896984d0c526432f2299fa&v=4 - url: https://github.com/viniciusCalcantara -- login: davidhuser - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/4357648?u=6ed702f8f6d49a8b2a0ed33cbd8ab59c2d7db7f7&v=4 - url: https://github.com/davidhuser -- login: dbfreem - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 - url: https://github.com/dbfreem -- login: SobikXexe - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/87701130?v=4 - url: https://github.com/SobikXexe -- login: pawelad - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/7062874?u=d27dc220545a8401ad21840590a97d474d7101e6&v=4 - url: https://github.com/pawelad -- login: Isuxiz - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/48672727?u=34d7b4ade252687d22a27cf53037b735b244bfc1&v=4 - url: https://github.com/Isuxiz -- login: Minibrams - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/8108085?u=b028dbc308fa8485e0e2e9402b3d03d8deb22bf9&v=4 - url: https://github.com/Minibrams -- login: adsouza +last_month_experts: +- login: YuriiMotov + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov +- login: Toygarmetu + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/92878791?u=538530cb6d5554e71f9c28709d794db9a74d23d9&v=4 + url: https://github.com/Toygarmetu +- login: JavierSanchezCastro + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: valentinDruzhinin count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/275832?u=f90f110cfafeafed2f14339e840941c2c328c186&v=4 - url: https://github.com/adsouza -- login: Synrom + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin +three_months_experts: +- login: YuriiMotov + count: 77 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov +- login: tiangolo + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: Toygarmetu + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/92878791?u=538530cb6d5554e71f9c28709d794db9a74d23d9&v=4 + url: https://github.com/Toygarmetu +- login: JavierSanchezCastro + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: ceb10n + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: valentinDruzhinin + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin +- login: RichieB2B + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: sachinh35 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 + url: https://github.com/sachinh35 +- login: EmmanuelNiyonshuti count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/30272537?v=4 - url: https://github.com/Synrom -- login: gaby + avatarUrl: https://avatars.githubusercontent.com/u/142030687?u=ab131d5ad4670280a978f489babe71c9bf9c1097&v=4 + url: https://github.com/EmmanuelNiyonshuti +- login: luzzodev count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/835733?u=8c72dec16fa560bdc81113354f2ffd79ad062bde&v=4 - url: https://github.com/gaby -- login: Ale-Cas + avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 + url: https://github.com/luzzodev +- login: davidbrochart count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/64859146?u=d52a6ecf8d83d2927e2ae270bdfcc83495dba8c9&v=4 - url: https://github.com/Ale-Cas -- login: CharlesPerrotMinotHCHB + avatarUrl: https://avatars.githubusercontent.com/u/4711805?u=d39696d995a9e02ec3613ffb2f62b20b14f92f26&v=4 + url: https://github.com/davidbrochart +- login: CharlieReitzel count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/112571330?u=e3a666718ff5ad1d1c49d6c31358a9f80c841b30&v=4 - url: https://github.com/CharlesPerrotMinotHCHB -- login: yanggeorge + avatarUrl: https://avatars.githubusercontent.com/u/20848272?v=4 + url: https://github.com/CharlieReitzel +- login: dotmitsu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2434407?v=4 - url: https://github.com/yanggeorge -- login: Brikas - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/80290187?v=4 - url: https://github.com/Brikas + avatarUrl: https://avatars.githubusercontent.com/u/42657211?u=3bccc9a2f386a3f24230ec393080f8904fe2a5b2&v=4 + url: https://github.com/dotmitsu - login: dolfinus count: 2 avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=ed5ddadcf36d9b943ebe61febe0b96ee34e5425d&v=4 url: https://github.com/dolfinus -- login: slafs +- login: garg-khushi count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 - url: https://github.com/slafs -- login: purepani + avatarUrl: https://avatars.githubusercontent.com/u/139839680?u=7faffa70275f8ab16f163e0c742a11d2662f9c66&v=4 + url: https://github.com/garg-khushi +- login: florentx count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/7587353?v=4 - url: https://github.com/purepani -- login: ddahan + avatarUrl: https://avatars.githubusercontent.com/u/142113?u=bf10f10080026346b092633c380977b61cee0d9c&v=4 + url: https://github.com/florentx +- login: JunjieAraoXiong count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1933516?u=1d200a620e8d6841df017e9f2bb7efb58b580f40&v=4 - url: https://github.com/ddahan -- login: TaigoFr + avatarUrl: https://avatars.githubusercontent.com/u/167785867?u=b69afe090c8bf5fd73f2d23fc3a887b28f68f192&v=4 + url: https://github.com/JunjieAraoXiong +six_months_experts: +- login: YuriiMotov + count: 150 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov +- login: tiangolo + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: luzzodev + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 + url: https://github.com/luzzodev +- login: engripaye + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/155247530?u=645169bc81856b7f1bd20090ecb0171a56dcbeb4&v=4 + url: https://github.com/engripaye +- login: JavierSanchezCastro + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: Toygarmetu + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/92878791?u=538530cb6d5554e71f9c28709d794db9a74d23d9&v=4 + url: https://github.com/Toygarmetu +- login: valentinDruzhinin + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin +- login: ceb10n + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: RichieB2B + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: JunjieAraoXiong + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/167785867?u=b69afe090c8bf5fd73f2d23fc3a887b28f68f192&v=4 + url: https://github.com/JunjieAraoXiong +- login: CodeKraken-cmd + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/48470371?u=e7c0e7ec8e35ca5fb3ae40a586ed5e788fd0fe6d&v=4 + url: https://github.com/CodeKraken-cmd +- login: svlandeg + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg +- login: ArmanShirzad + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/68951175?u=1f1efae2fa5d0d17c38a1a8413bedca5e538cedb&v=4 + url: https://github.com/ArmanShirzad +- login: krylosov-aa + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/242901957?u=4c9c7b468203b09bca64936fb464620e32cdd252&v=4 + url: https://github.com/krylosov-aa +- login: sachinh35 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 + url: https://github.com/sachinh35 +- login: simone-trubian + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/5606840?u=65703af3c605feca61ce49e4009bb4e26495b425&v=4 + url: https://github.com/simone-trubian +- login: mahimairaja + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/81288263?u=4eef6b4a36b96e84bd666fc1937aa589036ccb9a&v=4 + url: https://github.com/mahimairaja +- login: pankeshpatel + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1482917?u=666f39197a88cfa38b8bd78d39ef04d95c948b6b&v=4 + url: https://github.com/pankeshpatel +- login: huynguyengl99 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17792131?u=372b27056ec82f1ae03d8b3f37ef55b04a7cfdd1&v=4 - url: https://github.com/TaigoFr -- login: Garrett-R + avatarUrl: https://avatars.githubusercontent.com/u/49433085?u=7b626115686c5d97a2a32a03119f5300e425cc9f&v=4 + url: https://github.com/huynguyengl99 +- login: EmmanuelNiyonshuti count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/6614695?u=c128fd775002882f6e391bda5a89d1bdc5bdf45f&v=4 - url: https://github.com/Garrett-R -- login: jd-solanki + avatarUrl: https://avatars.githubusercontent.com/u/142030687?u=ab131d5ad4670280a978f489babe71c9bf9c1097&v=4 + url: https://github.com/EmmanuelNiyonshuti +- login: davidbrochart count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/47495003?u=6e225cb42c688d0cd70e65c6baedb9f5922b1178&v=4 - url: https://github.com/jd-solanki -- login: EverStarck + avatarUrl: https://avatars.githubusercontent.com/u/4711805?u=d39696d995a9e02ec3613ffb2f62b20b14f92f26&v=4 + url: https://github.com/davidbrochart +- login: CharlieReitzel count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/51029456?u=343409b7cb6b3ea6a59359f4e8370d9c3f140ecd&v=4 - url: https://github.com/EverStarck -- login: henrymcl + avatarUrl: https://avatars.githubusercontent.com/u/20848272?v=4 + url: https://github.com/CharlieReitzel +- login: dotmitsu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/26480299?v=4 - url: https://github.com/henrymcl + avatarUrl: https://avatars.githubusercontent.com/u/42657211?u=3bccc9a2f386a3f24230ec393080f8904fe2a5b2&v=4 + url: https://github.com/dotmitsu +- login: dolfinus + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=ed5ddadcf36d9b943ebe61febe0b96ee34e5425d&v=4 + url: https://github.com/dolfinus +- login: Kludex + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 + url: https://github.com/Kludex +- login: garg-khushi + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/139839680?u=7faffa70275f8ab16f163e0c742a11d2662f9c66&v=4 + url: https://github.com/garg-khushi +- login: skion + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/532192?v=4 + url: https://github.com/skion +- login: florentx + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/142113?u=bf10f10080026346b092633c380977b61cee0d9c&v=4 + url: https://github.com/florentx +- login: jc-louis + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/51329768?v=4 + url: https://github.com/jc-louis +- login: WilliamDEdwards + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 + url: https://github.com/WilliamDEdwards +- login: bughuntr7 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/236391583?u=7f51ff690e3a5711f845a115903c39e21c8af938&v=4 + url: https://github.com/bughuntr7 - login: jymchng count: 2 avatarUrl: https://avatars.githubusercontent.com/u/27895426?u=fb88c47775147d62a395fdb895d1af4148c7b566&v=4 url: https://github.com/jymchng -- login: christiansicari +- login: XieJiSS count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/29756552?v=4 - url: https://github.com/christiansicari -- login: JacobHayes + avatarUrl: https://avatars.githubusercontent.com/u/24671280?u=7ea0d9bfe46cf762594d62fd2f3c6d3813c3584c&v=4 + url: https://github.com/XieJiSS +- login: profatsky count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2555532?u=354a525847a276bbb4426b0c95791a8ba5970f9b&v=4 - url: https://github.com/JacobHayes -- login: iloveitaly + avatarUrl: https://avatars.githubusercontent.com/u/92920843?u=81e54bb0b613c171f7cd0ab3cbb58873782c9c9c&v=4 + url: https://github.com/profatsky +one_year_experts: +- login: YuriiMotov + count: 906 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov +- login: luzzodev + count: 62 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 + url: https://github.com/luzzodev +- login: tiangolo + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: valentinDruzhinin + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin +- login: alv2017 + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 +- login: JavierSanchezCastro + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: engripaye + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/155247530?u=645169bc81856b7f1bd20090ecb0171a56dcbeb4&v=4 + url: https://github.com/engripaye +- login: sachinh35 + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 + url: https://github.com/sachinh35 +- login: yauhen-sobaleu + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/51629535?u=fc1817060daf2df438bfca86c44f33da5cd667db&v=4 + url: https://github.com/yauhen-sobaleu +- login: Toygarmetu + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/92878791?u=538530cb6d5554e71f9c28709d794db9a74d23d9&v=4 + url: https://github.com/Toygarmetu +- login: yinziyan1206 + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 +- login: Kludex + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 + url: https://github.com/Kludex +- login: raceychan + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/75417963?u=060c62870ec5a791765e63ac20d8885d11143786&v=4 + url: https://github.com/raceychan +- login: ceb10n + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: RichieB2B + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: JunjieAraoXiong + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/167785867?u=b69afe090c8bf5fd73f2d23fc3a887b28f68f192&v=4 + url: https://github.com/JunjieAraoXiong +- login: CodeKraken-cmd + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/48470371?u=e7c0e7ec8e35ca5fb3ae40a586ed5e788fd0fe6d&v=4 + url: https://github.com/CodeKraken-cmd +- login: svlandeg + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg +- login: DoctorJohn + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/14076775?u=ec43fe79a98dbc864b428afc7220753e25ca3af2&v=4 + url: https://github.com/DoctorJohn +- login: WilliamDEdwards + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 + url: https://github.com/WilliamDEdwards +- login: ArmanShirzad + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/68951175?u=1f1efae2fa5d0d17c38a1a8413bedca5e538cedb&v=4 + url: https://github.com/ArmanShirzad +- login: krylosov-aa + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/242901957?u=4c9c7b468203b09bca64936fb464620e32cdd252&v=4 + url: https://github.com/krylosov-aa +- login: isgin01 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=16d6466476cf7dbc55a4cd575b6ea920ebdd81e1&v=4 + url: https://github.com/isgin01 +- login: sinisaos + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 + url: https://github.com/sinisaos +- login: dolfinus + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=ed5ddadcf36d9b943ebe61febe0b96ee34e5425d&v=4 + url: https://github.com/dolfinus +- login: jymchng + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/27895426?u=fb88c47775147d62a395fdb895d1af4148c7b566&v=4 + url: https://github.com/jymchng +- login: simone-trubian + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/5606840?u=65703af3c605feca61ce49e4009bb4e26495b425&v=4 + url: https://github.com/simone-trubian +- login: mahimairaja + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/81288263?u=4eef6b4a36b96e84bd666fc1937aa589036ccb9a&v=4 + url: https://github.com/mahimairaja +- login: pankeshpatel + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1482917?u=666f39197a88cfa38b8bd78d39ef04d95c948b6b&v=4 + url: https://github.com/pankeshpatel +- login: Jelle-tenB + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/210023470?u=c25d66addf36a747bd9fab773c4a6e7b238f45d4&v=4 + url: https://github.com/Jelle-tenB +- login: jgould22 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: stan-dot count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 - url: https://github.com/iloveitaly -- login: iiotsrc + avatarUrl: https://avatars.githubusercontent.com/u/56644812?u=a7dd773084f1c17c5f05019cc25a984e24873691&v=4 + url: https://github.com/stan-dot +- login: Damon0603 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/131771119?u=bcaf2559ef6266af70b151b7fda31a1ee3dbecb3&v=4 - url: https://github.com/iiotsrc -- login: PidgeyBE + avatarUrl: https://avatars.githubusercontent.com/u/110039208?u=f24bf5c30317bc4959118d1b919587c473a865b6&v=4 + url: https://github.com/Damon0603 +- login: huynguyengl99 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/19860056?u=47b584eb1c1ab45e31c1b474109a962d7e82be49&v=4 - url: https://github.com/PidgeyBE -- login: KianAnbarestani + avatarUrl: https://avatars.githubusercontent.com/u/49433085?u=7b626115686c5d97a2a32a03119f5300e425cc9f&v=4 + url: https://github.com/huynguyengl99 +- login: EmmanuelNiyonshuti count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/145364424?u=dcc3d8fb4ca07d36fb52a17f38b6650565de40be&v=4 - url: https://github.com/KianAnbarestani -- login: ykaiqx + avatarUrl: https://avatars.githubusercontent.com/u/142030687?u=ab131d5ad4670280a978f489babe71c9bf9c1097&v=4 + url: https://github.com/EmmanuelNiyonshuti +- login: Ale-Cas count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=1eebf5ce25a8067f7bfa6251a24f667be492d9d6&v=4 - url: https://github.com/ykaiqx -- login: AliYmn + avatarUrl: https://avatars.githubusercontent.com/u/64859146?u=d52a6ecf8d83d2927e2ae270bdfcc83495dba8c9&v=4 + url: https://github.com/Ale-Cas +- login: tiborrr count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/18416653?u=a77e2605e3ce6aaf6fef8ad4a7b0d32954fba47a&v=4 - url: https://github.com/AliYmn -- login: gelezo43 + avatarUrl: https://avatars.githubusercontent.com/u/16014746?u=0ce47015e53009e90393582fe86b7b90e809bc28&v=4 + url: https://github.com/tiborrr +- login: davidbrochart count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/40732698?u=611f39d3c1d2f4207a590937a78c1f10eed6232c&v=4 - url: https://github.com/gelezo43 -- login: jfeaver + avatarUrl: https://avatars.githubusercontent.com/u/4711805?u=d39696d995a9e02ec3613ffb2f62b20b14f92f26&v=4 + url: https://github.com/davidbrochart +- login: CharlieReitzel count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1091338?u=0bcba366447d8fadad63f6705a52d128da4c7ec2&v=4 - url: https://github.com/jfeaver + avatarUrl: https://avatars.githubusercontent.com/u/20848272?v=4 + url: https://github.com/CharlieReitzel +- login: kiranzo + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1070878?u=68f78a891c9751dd87571ac712a6309090c4bc01&v=4 + url: https://github.com/kiranzo +- login: dotmitsu + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/42657211?u=3bccc9a2f386a3f24230ec393080f8904fe2a5b2&v=4 + url: https://github.com/dotmitsu +- login: Brikas + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/80290187?u=2b72e497ca4444ecec1f9dc2d1b8d5437a27b83f&v=4 + url: https://github.com/Brikas +- login: BloodyRain2k + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1014362?v=4 + url: https://github.com/BloodyRain2k +- login: usiqwerty + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/37992525?u=0c6e91d7b3887aa558755f4225ce74a003cbe852&v=4 + url: https://github.com/usiqwerty +- login: garg-khushi + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/139839680?u=7faffa70275f8ab16f163e0c742a11d2662f9c66&v=4 + url: https://github.com/garg-khushi +- login: sk- + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/911768?u=3bfaf87089eb03ef0fa378f316b9c783f431aa9b&v=4 + url: https://github.com/sk- +- login: skion + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/532192?v=4 + url: https://github.com/skion +- login: Danstiv + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/50794055?v=4 + url: https://github.com/Danstiv +- login: florentx + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/142113?u=bf10f10080026346b092633c380977b61cee0d9c&v=4 + url: https://github.com/florentx +- login: jc-louis + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/51329768?v=4 + url: https://github.com/jc-louis +- login: bughuntr7 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/236391583?u=7f51ff690e3a5711f845a115903c39e21c8af938&v=4 + url: https://github.com/bughuntr7 +- login: purepani + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/7587353?v=4 + url: https://github.com/purepani +- login: asmaier + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/3169297?v=4 + url: https://github.com/asmaier +- login: henrymcl + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/26480299?v=4 + url: https://github.com/henrymcl +- login: potiuk + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/595491?v=4 + url: https://github.com/potiuk +- login: EverStarck + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/51029456?u=343409b7cb6b3ea6a59359f4e8370d9c3f140ecd&v=4 + url: https://github.com/EverStarck +- login: sanderbollen-clockworks + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/183479560?v=4 + url: https://github.com/sanderbollen-clockworks +- login: davidhuser + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/4357648?u=6ed702f8f6d49a8b2a0ed33cbd8ab59c2d7db7f7&v=4 + url: https://github.com/davidhuser +- login: XieJiSS + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/24671280?u=7ea0d9bfe46cf762594d62fd2f3c6d3813c3584c&v=4 + url: https://github.com/XieJiSS +- login: profatsky + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/92920843?u=81e54bb0b613c171f7cd0ab3cbb58873782c9c9c&v=4 + url: https://github.com/profatsky From 2f9c914d440e99fe8b62646b51e4d24c5bcd858b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Feb 2026 18:48:43 +0000 Subject: [PATCH 43/43] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2b068a5727..f56d34b5e7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👥 Update FastAPI People - Experts. PR [#14972](https://github.com/fastapi/fastapi/pull/14972) by [@tiangolo](https://github.com/tiangolo). * 👷 Allow skipping `benchmark` job in `test` workflow. PR [#14974](https://github.com/fastapi/fastapi/pull/14974) by [@YuriiMotov](https://github.com/YuriiMotov). ## 0.132.0