From b6c93e407388ffa48715cba4985b02600d6a4fbb Mon Sep 17 00:00:00 2001 From: beduldul Date: Mon, 26 Oct 2026 10:27:32 +0700 Subject: [PATCH] fix(types): keep plain JSON text in RichBlockPreformatted RichText.de_json treated any string that parses to a JSON object as a serialized RichText. A pre block holding a code sample like {"foo": "bar"} has no "type" key, so the lookup fell through to None and the block lost its text. Sending that message back failed with RICH_MESSAGE_CONTENT_REQUIRED. Only a dict that resolves to a known RichText counts as serialized now, and anything else keeps the original string. The type dispatch moved into _from_dict so the string branch can fall back to it. Fixes #2646. --- telebot/types.py | 20 ++++++++++++++------ tests/test_types.py | 13 +++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/telebot/types.py b/telebot/types.py index 0fd74bbf1..769f95514 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -14688,20 +14688,28 @@ def de_json(cls, json_string): if json_string is None: return None if isinstance(json_string, str): # "...can be either a String for plain text..." - # Only a JSON object is a serialized RichText. Plain text can look - # like JSON too, e.g. a "[56]" footnote mark, and stays text. + # Only a JSON object that resolves to a known RichText is serialized + # RichText. Plain text can look like JSON too, e.g. a "[56]" footnote + # mark or a {"foo": "bar"} code sample, and has to stay text. + original = json_string if not (json_string.startswith('{') and json_string.endswith('}')): - return json_string + return original try: parsed = json.loads(json_string) except (ValueError, TypeError): - return json_string + return original if not isinstance(parsed, dict): - return json_string - json_string = parsed + return original + result = cls._from_dict(parsed) + return original if result is None else result elif isinstance(json_string, list): # "...an Array of :class:`RichText`..." return [RichText.de_json(item) for item in json_string] + return cls._from_dict(json_string) + + @classmethod + def _from_dict(cls, json_string): + """Resolve a parsed RichText dict, or None when its type is unknown.""" # "...or any of the following types..." obj = cls.check_json(json_string) type = obj.pop('type', None) diff --git a/tests/test_types.py b/tests/test_types.py index cf18632eb..5927e1bfa 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -558,6 +558,19 @@ def test_json_richtext_plain_text_that_looks_like_json(): assert types.RichText.de_json(text) == text +def test_json_richtext_plain_json_object_stays_text(): + # issue #2646: a preformatted block whose text is a JSON object is still text + for text in ['{"foo": "bar"}', '{}', '{"a": [1, 2]}', '{"type": "not_a_richtext"}']: + block = types.RichBlockPreformatted.de_json({'text': text, 'language': 'json'}) + assert block.text == text + assert types.RichText.de_json(text) == text + + # a serialized RichText still deserializes + bold = types.RichText.de_json('{"type": "bold", "text": "hi"}') + assert isinstance(bold, types.RichTextBold) + assert bold.text == 'hi' + + def test_json_richtextitalic(): json_str = r'{"type": "italic", "text": "Test"}' result = types.RichText.de_json(json_str)