- Python 100%
Scrapes core.telegram.org/bots/api (no schema is published) into a custom JSON format: all types, methods, parameters and return types. Semantically tagged unions are recovered: discriminated ones (ChatMember, MessageOrigin, InputMedia, ...) via always/must-be field constants, field-presence ones (Update, CallbackQuery, ...) via the one-of-the-fields prose, plus untagged (MaybeInaccessibleMessage). Includes generated botapi.json for Bot API 10.1. Co-Authored-By: Eva |
||
|---|---|---|
| AGENTS.md | ||
| botapi.json | ||
| parse_botapi.py | ||
| README.md | ||
botapi_struct
A machine-readable JSON spec of the Telegram Bot API, scraped from the official reference page.
Telegram publishes no OpenAPI/JSON schema, so this project parses the docs HTML
instead — and, unlike a naive scrape, it recovers the tagged unions that the
prose only implies (Update, ChatMember, MessageOrigin, InputMedia, ...),
making the output directly usable for code generation of strongly-typed clients.
The format is custom (not OpenAPI): the Bot API doesn't map well onto OpenAPI
(discriminated unions by constant field values, unions by field presence,
Integer or String ad-hoc unions, True literal returns), so the spec models
those natively.
Files
| file | what |
|---|---|
parse_botapi.py |
the parser, Python 3 stdlib only |
botapi.json |
generated spec (committed for convenience) |
Usage
python3 parse_botapi.py # fetch the live page -> botapi.json
python3 parse_botapi.py --html page.html # parse a saved copy
python3 parse_botapi.py -o out.json
No dependencies. Diagnostics go to stderr: unresolvable type refs, unparsable
type expressions, and methods whose return type could not be extracted are
reported as warning: lines; a clean run prints only the summary line.
Output format
{
"meta": { "source": "https://core.telegram.org/bots/api", "bot_api_version": "10.1" },
"types": { "<TypeName>": Type, ... }, // insertion order = docs order
"methods": { "<methodName>": Method, ... }
}
Every type and method carries:
name— as in the docs (Update,sendMessage);href— docs anchor (https://core.telegram.org/bots/api#update);section— theh3heading it appears under (Available types,Payments, ...);description— markdown, with all links made absolute.
Type expressions (TypeExpr)
Used for struct fields, method parameters, union variants and return types.
| shape | meaning |
|---|---|
{"kind": "integer" | "float" | "string" | "boolean"} |
scalar |
{"kind": "literal", "value": true} |
Telegram's True pseudo-type |
{"kind": "ref", "name": "Message"} |
reference to an entry in types |
{"kind": "array", "item": TypeExpr} |
Array of X; nests (Array of Array of X) |
{"kind": "union", "alternatives": [TypeExpr, ...]} |
anonymous union (Integer or String, InputFile or String) |
Types
kind: "struct"
{
"kind": "struct",
"fields": [
{ "name": "status", "type": {"kind": "string"}, "required": true,
"description": "The member's status in the chat, always “creator”",
"const": "creator" } // only when the docs pin the value
]
}
requiredreflects theOptional.marker; the marker itself is stripped from the description.constis present when the docs fix the value:always “creator”,must be *photo*,Always 0(InaccessibleMessage.date), or a literalTrue-typed field. Strings stay strings, numbers become numbers.- Service-message markers that "hold no information" (
ForumTopicClosed,VideoChatStarted,CallbackGame, ...) are structs with emptyfields.
kind: "union"
Three flavors, distinguished by tag.kind:
field — classic discriminated union (ChatMember, MessageOrigin,
BotCommandScope, InputMedia, TransactionPartner, ...):
{
"kind": "union",
"tag": { "kind": "field", "field": "status" },
"variants": [
{ "tag": "creator", "type": {"kind": "ref", "name": "ChatMemberOwner"} },
{ "tag": "administrator", "type": {"kind": "ref", "name": "ChatMemberAdministrator"} }
// ...
]
}
Each tag equals the const of the discriminator field inside the variant
struct (the parser cross-checks this). tag.ambiguous: true marks unions where
tag values repeat — currently only InlineQueryResult, where cached and
non-cached results share type values and Telegram disambiguates them by which
fields are present. Serialization is unaffected; deserializers must look at
fields.
field_presence — Update-style union, where the active variant is chosen
by which optional field is set:
{
"kind": "union",
"tag": { "kind": "field_presence", "exclusivity": "at_most_one" },
"common_fields": [ { "name": "update_id", ... } ], // always-present part
"variants": [
{ "tag": "message", "type": {"kind": "ref", "name": "Message"}, "description": "..." },
{ "tag": "edited_message", "type": {"kind": "ref", "name": "Message"}, "description": "..." }
// ...
]
}
Detected from the "at most/exactly one of the (optional) fields" prose.
Currently: Update, PollMedia, KeyboardButton, InlineKeyboardButton,
CallbackQuery (data vs game_short_name), InlineQueryResultsButton,
InputRichMessage. With exclusivity: "at_most_one", none of the variants
is also a valid state; exactly_one requires exactly one.
untagged — no discriminator exists (MaybeInaccessibleMessage,
InputMessageContent); consumers must disambiguate structurally:
{
"kind": "union",
"tag": { "kind": "untagged" },
"variants": [ { "type": {"kind": "ref", "name": "Message"} }, ... ]
}
(For MaybeInaccessibleMessage the docs do pin InaccessibleMessage.date to
const: 0, which a deserializer can use.)
Structs that appear as variants of some union get a back-reference:
"subtype_of": ["ChatMember"].
kind: "opaque"
No structure in the docs. Currently only InputFile — the raw multipart file
upload; represent it however your HTTP layer needs.
Methods
{
"parameters": [
{ "name": "chat_id",
"type": {"kind": "union", "alternatives": [{"kind": "integer"}, {"kind": "string"}]},
"required": true,
"description": "..." }
],
"returns": {"kind": "ref", "name": "Message"}
}
parameterspreserves docs order; methods without a table (e.g.getMe) have an empty list.returnsis extracted from the "Returns X on success" / "... is returned" prose. TheeditMessage*family yields{"kind": "union", "alternatives": [Message, literal(true)]}from "...otherwise True is returned".null(with a warning) means the parser found no return sentence — treat as a parser bug.
How the union detection works
- Explicit unions: a type whose body is a
<ul>of bare type links. - Discriminator: a field shared by all variant structs whose description
pins a distinct constant per variant (
always “x”/must be *x*). Preference order when several qualify:type,status,source. - Field-presence unions: struct prose matching
"at most/exactly one of the optional fields",
"... one of the fields other than a, b", or
"... one of the fields a or b". All participants must be optional.
Note the docs put markup inside the phrase ("at most
<strong>one</strong>of ..."), so matching runs on plain text, not HTML.
Regenerating
The parser is heuristic over semi-structured HTML; after a Bot API release,
rerun it and review the stderr warnings — a new documentation pattern shows up
as unknown type ref, cannot parse type, or could not extract return type.