From 0d4ca48183058dd460d4164cea22f92ce55fc522 Mon Sep 17 00:00:00 2001 From: Shardul D Date: Sun, 28 Jun 2026 21:17:17 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Server-Sent=20Event=20data?= =?UTF-8?q?=20ignoring=20Pydantic=20serialization=5Falias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ServerSentEvent(data=)` 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 --- fastapi/routing.py | 8 +++++++- tests/test_sse.py | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 3a2d75422..06336afa3 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -523,7 +523,13 @@ def get_request_handler( data_str: str | None = item.raw_data elif item.data is not None: 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: data_str = json.dumps(jsonable_encoder(item.data)) else: diff --git a/tests/test_sse.py b/tests/test_sse.py index 6a9d669fe..15db1ca3d 100644 --- a/tests/test_sse.py +++ b/tests/test_sse.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, FastAPI from fastapi.responses import EventSourceResponse from fastapi.sse import ServerSentEvent from fastapi.testclient import TestClient -from pydantic import BaseModel +from pydantic import BaseModel, Field class Item(BaseModel): @@ -16,6 +16,10 @@ class Item(BaseModel): description: str | None = None +class AliasedItem(BaseModel): + name: str = Field(serialization_alias="itemName") + + items = [ Item(name="Plumbus", description="A multi-purpose household 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") +@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() @@ -216,6 +234,27 @@ def test_string_data_json_encoded(client: TestClient): 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(): with pytest.raises(ValueError, match="null"): ServerSentEvent(data="test", id="has\0null")