Browse Source

🐛 Fix Server-Sent Event data ignoring Pydantic serialization_alias

`ServerSentEvent(data=<pydantic model>)` was serialized with `model_dump_json()`, which defaults to `by_alias=False` in Pydantic v2, so `serialization_alias`/`alias` were ignored. Serialize with `by_alias=response_model_by_alias` to match the rest of FastAPI's response serialization (honors aliases by default, respects `response_model_by_alias=False`).

Reported in #15703
pull/15858/head
Shardul D 1 month ago
parent
commit
0d4ca48183
Failed to extract signature
  1. 8
      fastapi/routing.py
  2. 41
      tests/test_sse.py

8
fastapi/routing.py

@ -523,7 +523,13 @@ def get_request_handler(
data_str: str | None = item.raw_data data_str: str | None = item.raw_data
elif item.data is not None: elif item.data is not None:
if hasattr(item.data, "model_dump_json"): if hasattr(item.data, "model_dump_json"):
data_str = item.data.model_dump_json() # Thread `response_model_by_alias` (default `True`)
# so Pydantic `serialization_alias`/`alias` are
# honored, like the rest of FastAPI's response
# serialization. See #15703.
data_str = item.data.model_dump_json(
by_alias=response_model_by_alias
)
else: else:
data_str = json.dumps(jsonable_encoder(item.data)) data_str = json.dumps(jsonable_encoder(item.data))
else: else:

41
tests/test_sse.py

@ -8,7 +8,7 @@ from fastapi import APIRouter, FastAPI
from fastapi.responses import EventSourceResponse from fastapi.responses import EventSourceResponse
from fastapi.sse import ServerSentEvent from fastapi.sse import ServerSentEvent
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel from pydantic import BaseModel, Field
class Item(BaseModel): class Item(BaseModel):
@ -16,6 +16,10 @@ class Item(BaseModel):
description: str | None = None description: str | None = None
class AliasedItem(BaseModel):
name: str = Field(serialization_alias="itemName")
items = [ items = [
Item(name="Plumbus", description="A multi-purpose household device."), Item(name="Plumbus", description="A multi-purpose household device."),
Item(name="Portal Gun", description="A portal opening device."), Item(name="Portal Gun", description="A portal opening device."),
@ -87,6 +91,20 @@ async def sse_items_raw():
yield ServerSentEvent(raw_data="cpu,87.3,1709145600", event="csv") yield ServerSentEvent(raw_data="cpu,87.3,1709145600", event="csv")
@app.get("/items/stream-sse-event-alias", response_class=EventSourceResponse)
async def sse_items_event_alias():
yield ServerSentEvent(data=AliasedItem(name="Portal Gun"))
@app.get(
"/items/stream-sse-event-alias-disabled",
response_class=EventSourceResponse,
response_model_by_alias=False,
)
async def sse_items_event_alias_disabled():
yield ServerSentEvent(data=AliasedItem(name="Portal Gun"))
router = APIRouter() router = APIRouter()
@ -216,6 +234,27 @@ def test_string_data_json_encoded(client: TestClient):
assert 'data: "plain text data"\n' in response.text assert 'data: "plain text data"\n' in response.text
def test_sse_event_data_uses_serialization_alias(client: TestClient):
"""`ServerSentEvent(data=model)` must honor Pydantic `serialization_alias`.
Regression test for
https://github.com/fastapi/fastapi/discussions/15703
"""
response = client.get("/items/stream-sse-event-alias")
assert response.status_code == 200
assert 'data: {"itemName":"Portal Gun"}\n' in response.text
assert '"name"' not in response.text
def test_sse_event_data_respects_response_model_by_alias(client: TestClient):
"""`response_model_by_alias=False` disables aliasing for SSE event data too,
consistent with plainly-yielded items."""
response = client.get("/items/stream-sse-event-alias-disabled")
assert response.status_code == 200
assert 'data: {"name":"Portal Gun"}\n' in response.text
assert "itemName" not in response.text
def test_server_sent_event_null_id_rejected(): def test_server_sent_event_null_id_rejected():
with pytest.raises(ValueError, match="null"): with pytest.raises(ValueError, match="null"):
ServerSentEvent(data="test", id="has\0null") ServerSentEvent(data="test", id="has\0null")

Loading…
Cancel
Save