From b6c93e407388ffa48715cba4985b02600d6a4fbb Mon Sep 17 00:00:00 2001 From: beduldul Date: Mon, 26 Oct 2026 10:27:32 +0700 Subject: [PATCH 1/2] 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) From c5285aa9e4f5561e32e7de783368be0a627321e5 Mon Sep 17 00:00:00 2001 From: beduldul Date: Sat, 31 Oct 2026 22:00:10 +0700 Subject: [PATCH 2/2] Treat a partial RichText-shaped JSON object as plain text A JSON object whose "type" matches a known RichText but that is missing the rest of the shape, e.g. {"type": "bold"}, is ordinary text the user typed, but it raised KeyError: 'text' out of RichTextBold.de_json. Fall back to the original string when resolving the parsed object raises, which keeps the plain-text branch total. --- telebot/types.py | 7 ++++++- tests/test_types.py | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/telebot/types.py b/telebot/types.py index 769f95514..27c19ec5d 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -14700,7 +14700,12 @@ def de_json(cls, json_string): return original if not isinstance(parsed, dict): return original - result = cls._from_dict(parsed) + try: + result = cls._from_dict(parsed) + except (KeyError, TypeError, AttributeError): + # A JSON object whose "type" happens to match a RichText but + # whose shape does not is still plain text, not a crash. + return original return original if result is None else result elif isinstance(json_string, list): # "...an Array of :class:`RichText`..." diff --git a/tests/test_types.py b/tests/test_types.py index 5927e1bfa..ca830ecb8 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -571,6 +571,13 @@ def test_json_richtext_plain_json_object_stays_text(): assert bold.text == 'hi' +def test_json_richtext_partial_json_object_does_not_crash(): + # a JSON object whose "type" matches a RichText but whose shape does not is + # still plain text; it used to raise KeyError: 'text' + for text in ['{"type": "bold"}', '{"type": "italic"}', '{"type": "code"}']: + assert types.RichText.de_json(text) == text + + def test_json_richtextitalic(): json_str = r'{"type": "italic", "text": "Test"}' result = types.RichText.de_json(json_str)