From 2b549d99b69a79caf91502081306d2ec14928059 Mon Sep 17 00:00:00 2001 From: Lukas Erlbacher Date: Mon, 2 Feb 2026 16:00:43 +0100 Subject: [PATCH] fixed bug introduced in 0.124.1 --- fastapi/_compat/v2.py | 4 +- tests/test_union_single_root_model_openapi.py | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/test_union_single_root_model_openapi.py diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 25b6814536..a2a92d5e09 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -22,7 +22,7 @@ from pydantic import ValidationError as ValidationError from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] GetJsonSchemaHandler as GetJsonSchemaHandler, ) -from pydantic._internal._typing_extra import eval_type_lenient +from pydantic._internal._typing_extra import annotated_type, eval_type_lenient from pydantic._internal._utils import lenient_issubclass as lenient_issubclass from pydantic.fields import FieldInfo as FieldInfo from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema @@ -460,7 +460,7 @@ def create_body_model( def get_model_fields(model: type[BaseModel]) -> list[ModelField]: model_fields: list[ModelField] = [] for name, field_info in model.model_fields.items(): - type_ = field_info.annotation + type_ = annotated_type(field_info.annotation) or field_info.annotation if lenient_issubclass(type_, (BaseModel, dict)) or is_dataclass(type_): model_config = None else: diff --git a/tests/test_union_single_root_model_openapi.py b/tests/test_union_single_root_model_openapi.py new file mode 100644 index 0000000000..a2747074aa --- /dev/null +++ b/tests/test_union_single_root_model_openapi.py @@ -0,0 +1,88 @@ +from typing import Annotated, Literal, Union + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +@pytest.fixture(name="client") +def get_client(): + from pydantic import BaseModel, Field, RootModel + + class Base(BaseModel): + type: Literal["BASE"] = "BASE" + value: str + + class MyModel(RootModel[Annotated[Union[Base], Field(discriminator="type")]]): + pass + + app = FastAPI() + + @app.get("/") + def test() -> MyModel: + return MyModel.model_validate(Base(value="test")) + + client = TestClient(app) + return client + + +def test_get(client: TestClient): + response = client.get("/") + assert response.json() == {"value": "test", "type": "BASE"} + + +def test_openapi_schema(client: TestClient): + response = client.get("openapi.json") + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Test", + "operationId": "test__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyModel" + } + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Base": { + "properties": { + "type": { + "type": "string", + "const": "BASE", + "title": "Type", + "default": "BASE", + }, + "value": {"type": "string", "title": "Value"}, + }, + "type": "object", + "required": ["value"], + "title": "Base", + }, + "MyModel": { + "oneOf": [{"$ref": "#/components/schemas/Base"}], + "title": "MyModel", + "discriminator": { + "propertyName": "type", + "mapping": {"BASE": "#/components/schemas/Base"}, + }, + }, + } + }, + } + )