diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a5761361d..b3e53b91c 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -54,7 +54,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -96,7 +96,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index ee8bfafb4..7d5449c6a 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 2c432da8c..0eb26cc4d 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 76ac77298..e6ae3d963 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -26,7 +26,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index ef3990d31..04beeb64e 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -34,7 +34,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index e6e56bf04..f15b92137 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index cde0ca308..e42d79723 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -26,7 +26,7 @@ jobs: with: python-version: '3.9' - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 1e245346d..7d29469a5 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -30,7 +30,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 622cd52e9..cbf1a8567 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -71,7 +71,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -116,7 +116,7 @@ jobs: with: python-version: '3.8' - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml index cb98698d3..22b37d59d 100644 --- a/.github/workflows/topic-repos.yml +++ b/.github/workflows/topic-repos.yml @@ -25,7 +25,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index fa4e8f463..a7fcf84df 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -48,7 +48,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7d7c7ec5a..6dfc92cbc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,16 @@ hide: ## Latest Changes +## 0.118.2 + +### Fixes + +* 🐛 Fix tagged discriminated union not recognized as body field. PR [#12942](https://github.com/fastapi/fastapi/pull/12942) by [@frankie567](https://github.com/frankie567). + +### Internal + +* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#14167](https://github.com/fastapi/fastapi/pull/14167) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.118.1 ### Upgrades diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 12ebbaf9b..cd606969c 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.118.1" +__version__ = "0.118.2" from starlette import status as status diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 8ea5bf253..21ea1a237 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -590,6 +590,9 @@ def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: if origin is Union or origin is UnionType: return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) + if origin is Annotated: + return field_annotation_is_complex(get_args(annotation)[0]) + return ( _annotation_is_complex(annotation) or _annotation_is_complex(origin) diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py new file mode 100644 index 000000000..6af9e1d22 --- /dev/null +++ b/tests/test_union_body_discriminator.py @@ -0,0 +1,188 @@ +from typing import Any, Dict, Union + +from dirty_equals import IsDict +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +from .utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_discriminator_pydantic_v2() -> None: + from pydantic import Tag + + app = FastAPI() + + class FirstItem(BaseModel): + value: Literal["first"] + price: int + + class OtherItem(BaseModel): + value: Literal["other"] + price: float + + Item = Annotated[ + Union[Annotated[FirstItem, Tag("first")], Annotated[OtherItem, Tag("other")]], + Field(discriminator="value"), + ] + + @app.post("/items/") + def save_union_body_discriminator( + item: Item, q: Annotated[str, Field(description="Query string")] + ) -> Dict[str, Any]: + return {"item": item} + + client = TestClient(app) + response = client.post("/items/?q=first", json={"value": "first", "price": 100}) + assert response.status_code == 200, response.text + assert response.json() == {"item": {"value": "first", "price": 100}} + + response = client.post("/items/?q=other", json={"value": "other", "price": 100.5}) + assert response.status_code == 200, response.text + assert response.json() == {"item": {"value": "other", "price": 100.5}} + + 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/": { + "post": { + "summary": "Save Union Body Discriminator", + "operationId": "save_union_body_discriminator_items__post", + "parameters": [ + { + "name": "q", + "in": "query", + "required": True, + "schema": { + "type": "string", + "description": "Query string", + "title": "Q", + }, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "oneOf": [ + {"$ref": "#/components/schemas/FirstItem"}, + {"$ref": "#/components/schemas/OtherItem"}, + ], + "discriminator": { + "propertyName": "value", + "mapping": { + "first": "#/components/schemas/FirstItem", + "other": "#/components/schemas/OtherItem", + }, + }, + "title": "Item", + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": IsDict( + { + # Pydantic 2.10, in Python 3.8 + # TODO: remove when dropping support for Python 3.8 + "type": "object", + "title": "Response Save Union Body Discriminator Items Post", + } + ) + | IsDict( + { + "type": "object", + "additionalProperties": True, + "title": "Response Save Union Body Discriminator Items Post", + } + ) + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "FirstItem": { + "properties": { + "value": { + "type": "string", + "const": "first", + "title": "Value", + }, + "price": {"type": "integer", "title": "Price"}, + }, + "type": "object", + "required": ["value", "price"], + "title": "FirstItem", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "OtherItem": { + "properties": { + "value": { + "type": "string", + "const": "other", + "title": "Value", + }, + "price": {"type": "number", "title": "Price"}, + }, + "type": "object", + "required": ["value", "price"], + "title": "OtherItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + )