Skip to content

fix(types): keep plain JSON text in RichBlockPreformatted - #2647

Merged
Badiboy merged 2 commits into
eternnoir:masterfrom
beduldul:fix/richtext-plain-json-text
Oct 31, 2026
Merged

Badiboy merged 2 commits into
eternnoir:masterfrom
beduldul:fix/richtext-plain-json-text

Conversation

@beduldul

Copy link
Copy Markdown
Contributor

Description

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 dispatch fell through to None and the block lost its text:

>>> types.RichBlockPreformatted.de_json({'text': '{"foo": "bar"}', 'language': 'json'}).text
None

Sending that message back then fails with RICH_MESSAGE_CONTENT_REQUIRED (#2646).

Only a dict that resolves to a known RichText counts as serialized now; anything else keeps the original string. The type dispatch moved into _from_dict so the string branch can fall back to it. The [56] case from #2635 still works, and so do {}, {"a": [1, 2]} and {"type": "not_a_richtext"}.

Describe your tests

Added test_json_richtext_plain_json_object_stays_text in tests/test_types.py. It covers the preformatted block from the issue plus the other JSON-shaped strings, and asserts a real serialized RichText still deserializes.

Full suite: 308 passed, 73 skipped.

Python version: 3.12.3

OS: macOS 27.2

Checklist:

  • I added/edited example on new feature/change (if exists)
  • My changes won't break backward compatibility
  • I made changes both for sync and async

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 eternnoir#2646.
@All-The-Foxes

All-The-Foxes commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

What I did to fix it locally in the meantime is add

if 'type' not in parsed:
	return json_string

before the json_string = parsed line. However this is not bulletproof either because a plain string with 'type' as a top level field could cause the same issue. When dumping the json of a Telegram message into a rich message, which is what I do because even with null filtering they can often exceed the allowed limit on a standard message, you will get literally everything from the message in the form of a plain text json string, which includes things that match the filtering checks and whatnot. For that reason I think the best option is to restore the old semantic boundary of

if isinstance(json_string, str):
	return json_string

@Badiboy

Badiboy commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

The approach made by Telegram:

This object represents a rich formatted text. Currently, it can be either a String for plain text, an Array of RichText, or any of the following types:

is really a pain-in-the-ass....

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the change preserving JSON-shaped plain text in rich-message blocks.

No open issues 🎉

What changed in this PR

Fixes JSON-shaped plain text being lost from preformatted rich-message blocks, addressing #2646 in the shared type handling used by synchronous and asynchronous bots.

Changes:

  • Preserves JSON-object strings as plain text unless they resolve to a known RichText type.
  • Extracts type dispatch into _from_dict.
  • Adds regression coverage for JSON code samples while confirming serialized RichText still deserializes.
File Description
tests/​test_types.py Adds tests for preserving JSON-shaped text and deserializing valid RichText.
telebot/​types.py Adds a plain-text fallback and extracts dictionary-based RichText dispatch.

Review effort: Balanced

@All-The-Foxes

Copy link
Copy Markdown
Contributor

The approach made by Telegram:

This object represents a rich formatted text. Currently, it can be either a String for plain text, an Array of RichText, or any of the following types:

is really a pain-in-the-ass....

If it's a string, treat it as a string. There shouldn't be any special handling required for that. Assign it to 'text' and don't try to serialize it or anything if the instance is string. There shouldn't be a situation where a valid RichText object arrives to the 'text' variable as a string, it would be a dict or something else.

@All-The-Foxes

All-The-Foxes commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Check for string instance first, if it is then return it as-is. Otherwise it's an array or a RichText* instance so then you can de-json it. Otherwise you'll always have instances where things match when they're not supposed to, for example dumping json of a Telegram message while debugging a bot. If you're dumping the json of a rich message then it's going to look like a valid rich message because... it is. But it's also a string meant to be sent, so it should just be returned and not processed.

I ran into this issue when I was just sending json in a rich message to test all of the rich message features and make sure I implemented them correctly. But no matter what conditions you test the string for, it's still a string. It should just be returned. Otherwise you can have anyone break a bot by sending a rich message's json to it and that shouldn't be possible.

@Badiboy

Badiboy commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

RichText is JsonDeserializable. It may come as JSON string, that is the root of evil. Thus we cannot learly dedicate string from json-string.

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.
@beduldul

Copy link
Copy Markdown
Contributor Author

On the objection that a string cannot be cleanly separated from a JSON-string: agreed in general, but the current branch already narrows "serialized RichText" to objects that resolve to a known type, and that leaves one hole I hit while testing.

RichText.de_json('{"type": "bold"}') raised KeyError: 'text', because the type matches a known RichText while the rest of the shape is missing. That is plain text a user typed, and it crashed instead of staying text.

c5285aa makes that branch total: if resolving the parsed object raises, the original string is returned. So the rule is now "a JSON object is RichText only if it fully resolves", not "only if its type looks right".

Test added for the partial-shape case; suite is 309 passed, 73 skipped.

@Badiboy

Badiboy commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

I'm really tired of this pain made by Tg. Let's try keeping this another "optimization", may be it will cover the pain better.

@Badiboy
Badiboy merged commit 469eece into eternnoir:master Oct 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants