diff --git a/aiogram/methods/base.py b/aiogram/methods/base.py index b2d35bec..66d4dde6 100644 --- a/aiogram/methods/base.py +++ b/aiogram/methods/base.py @@ -12,11 +12,16 @@ from typing import ( TypeVar, ) -from pydantic import BaseModel, ConfigDict -from pydantic.functional_validators import model_validator +from pydantic import ( + BaseModel, + ConfigDict, + SerializerFunctionWrapHandler, + model_serializer, + model_validator, +) from aiogram.client.context_controller import BotContextController - +from aiogram.client.default import Default, DefaultBotProperties from ..types import InputFile, ResponseParameters from ..types.base import UNSET_TYPE @@ -65,6 +70,22 @@ class TelegramMethod(BotContextController, BaseModel, Generic[TelegramType], ABC return values return {k: v for k, v in values.items() if not isinstance(v, UNSET_TYPE)} + @model_serializer(mode="wrap", when_used="json") + def json_serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]: + """ + Replacing `Default` placeholders with actual values from bot defaults. + Ensures JSON serialization backward compatibility by handling non-standard objects. + """ + if not isinstance(self, TelegramMethod): + return handler(self) + properties = self.bot.default if self.bot else DefaultBotProperties() + default_fields = { + field: properties[value.name] + for field in self.model_fields.keys() + if isinstance(value := getattr(self, field), Default) + } + return handler(self.model_copy(update=default_fields)) + if TYPE_CHECKING: __returning__: ClassVar[type] __api_method__: ClassVar[str] diff --git a/aiogram/types/base.py b/aiogram/types/base.py index f97cf03a..aa13c80a 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -1,10 +1,16 @@ from typing import Any, Dict from unittest.mock import sentinel -from pydantic import BaseModel, ConfigDict, model_validator +from pydantic import ( + BaseModel, + ConfigDict, + SerializerFunctionWrapHandler, + model_serializer, + model_validator, +) from aiogram.client.context_controller import BotContextController -from aiogram.client.default import Default +from aiogram.client.default import Default, DefaultBotProperties class TelegramObject(BotContextController, BaseModel): @@ -36,6 +42,22 @@ class TelegramObject(BotContextController, BaseModel): return values return {k: v for k, v in values.items() if not isinstance(v, UNSET_TYPE)} + @model_serializer(mode="wrap", when_used="json") + def json_serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]: + """ + Replacing `Default` placeholders with actual values from bot defaults. + Ensures JSON serialization backward compatibility by handling non-standard objects. + """ + if not isinstance(self, TelegramObject): + return handler(self) + properties = self.bot.default if self.bot else DefaultBotProperties() + default_fields = { + field: properties[value.name] + for field in self.model_fields.keys() + if isinstance(value := getattr(self, field), Default) + } + return handler(self.model_copy(update=default_fields)) + class MutableTelegramObject(TelegramObject): model_config = ConfigDict( diff --git a/tests/test_api/test_methods/test_base.py b/tests/test_api/test_methods/test_base.py index 085650f3..66cfd101 100644 --- a/tests/test_api/test_methods/test_base.py +++ b/tests/test_api/test_methods/test_base.py @@ -1,9 +1,11 @@ +from typing import Any, Dict from unittest.mock import sentinel import pytest -from aiogram.methods import GetMe, TelegramMethod -from aiogram.types import TelegramObject, User +from aiogram.client.default import Default +from aiogram.methods import GetMe, SendMessage, TelegramMethod +from aiogram.types import LinkPreviewOptions, TelegramObject, User from tests.mocked_bot import MockedBot @@ -26,6 +28,25 @@ class TestTelegramMethodRemoveUnset: assert obj.remove_unset("") == "" +class TestTelegramMethodJsonSerialize: + @pytest.mark.parametrize( + "obj", + [ + SendMessage( + chat_id=1, + text="test", + ), + LinkPreviewOptions(), + ], + ) + def test_json_serialize(self, obj): + def has_defaults(dump: Dict[str, Any]) -> bool: + return any(isinstance(value, Default) for value in dump.values()) + + assert has_defaults(obj.model_dump()) + assert not has_defaults(obj.model_dump(mode="json")) + + class TestTelegramMethodCall: async def test_async_emit_unsuccessful(self, bot: MockedBot): with pytest.raises(