Browse Source

🐛 Fix named PEP 695 type alias used as body parameter losing its OpenAPI component ($ref)

A bare PEP 695 type alias used directly as a request-body parameter generated an inline OpenAPI schema, while the equivalent `Annotated[Alias, Body()]` form produced a named $ref. Preserve the original alias when building the inferred body field so both forms emit the same named component.

Fixes #15855
pull/15856/head
Shardul D 3 weeks ago
parent
commit
a10cd70660
Failed to extract signature
  1. 9
      fastapi/dependencies/utils.py
  2. 69
      tests/test_dependency_pep695.py

9
fastapi/dependencies/utils.py

@ -401,8 +401,11 @@ def analyze_param(
depends = None
type_annotation: Any = Any
use_annotation: Any = Any
type_alias_annotation = None
if is_typealiastype(annotation):
# unpack in case PEP 695 type syntax is used
# Unpack PEP 695 type aliases for the analysis below, but keep the original
# alias so an inferred body can still produce a named OpenAPI component.
type_alias_annotation = annotation
annotation = annotation.__value__
if annotation is not inspect.Signature.empty:
use_annotation = annotation
@ -512,6 +515,10 @@ def analyze_param(
) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=use_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation):
if type_alias_annotation is not None:
# Use the original PEP 695 alias as the body annotation so it yields a
# named OpenAPI component ($ref), like `Annotated[Alias, Body()]` (#15855).
use_annotation = type_alias_annotation
field_info = params.Body(annotation=use_annotation, default=default_value)
else:
field_info = params.Query(annotation=use_annotation, default=default_value)

69
tests/test_dependency_pep695.py

@ -1,7 +1,8 @@
from typing import Annotated
from typing import Annotated, Literal
from fastapi import Depends, FastAPI
from fastapi import Body, Depends, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from typing_extensions import TypeAliasType
@ -25,3 +26,67 @@ def test_pep695_type_dependencies():
response = client.get("/")
assert response.status_code == 200
assert response.text == '"value: 123"'
def test_pep695_type_alias_body_keeps_named_ref():
"""A bare PEP 695 ``TypeAliasType`` used as a body parameter should generate
the same named OpenAPI component (``$ref``) as the equivalent
``Annotated[Alias, Body()]`` form.
Regression test for https://github.com/fastapi/fastapi/issues/15855
"""
app = FastAPI()
class Cat(BaseModel):
pet_type: Literal["cat"]
meows: int
class Dog(BaseModel):
pet_type: Literal["dog"]
barks: float
Pet = TypeAliasType(
"Pet",
Annotated[Cat | Dog, Field(discriminator="pet_type")],
type_params=(),
)
@app.post("/bare")
def bare(pet: Pet):
return pet
@app.post("/body")
def body(pet: Annotated[Pet, Body()]):
return pet
client = TestClient(app)
schema = client.get("/openapi.json").json()
def request_body_schema(path: str):
return schema["paths"][path]["post"]["requestBody"]["content"][
"application/json"
]["schema"]
expected_ref = {"$ref": "#/components/schemas/Pet"}
# The explicit Annotated[..., Body()] form already keeps the named ref today.
assert request_body_schema("/body") == expected_ref
# The bare alias form must produce the same named ref (was inlined before the fix).
assert request_body_schema("/bare") == expected_ref
assert "Pet" in schema["components"]["schemas"]
# The discriminated union still validates and serializes correctly.
response = client.post("/bare", json={"pet_type": "cat", "meows": 3})
assert response.status_code == 200, response.text
assert response.json() == {"pet_type": "cat", "meows": 3}
response = client.post("/bare", json={"pet_type": "dog", "barks": 1.5})
assert response.status_code == 200, response.text
assert response.json() == {"pet_type": "dog", "barks": 1.5}
response = client.post("/bare", json={"pet_type": "lizard"})
assert response.status_code == 422, response.text
# The explicit Annotated[..., Body()] form behaves identically.
response = client.post("/body", json={"pet_type": "dog", "barks": 1.5})
assert response.status_code == 200, response.text
assert response.json() == {"pet_type": "dog", "barks": 1.5}

Loading…
Cancel
Save