BotAPI structure parsed
Find a file
Aleksandr 6d5433e3be Telegram Bot API docs parser producing a structured JSON spec
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
2026-07-13 20:02:46 +03:00
AGENTS.md Telegram Bot API docs parser producing a structured JSON spec 2026-07-13 20:02:46 +03:00
botapi.json Telegram Bot API docs parser producing a structured JSON spec 2026-07-13 20:02:46 +03:00
parse_botapi.py Telegram Bot API docs parser producing a structured JSON spec 2026-07-13 20:02:46 +03:00
README.md Telegram Bot API docs parser producing a structured JSON spec 2026-07-13 20:02:46 +03:00

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 — the h3 heading 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
  ]
}
  • required reflects the Optional. marker; the marker itself is stripped from the description.
  • const is present when the docs fix the value: always “creator”, must be *photo*, Always 0 (InaccessibleMessage.date), or a literal True-typed field. Strings stay strings, numbers become numbers.
  • Service-message markers that "hold no information" (ForumTopicClosed, VideoChatStarted, CallbackGame, ...) are structs with empty fields.

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"}
}
  • parameters preserves docs order; methods without a table (e.g. getMe) have an empty list.
  • returns is extracted from the "Returns X on success" / "... is returned" prose. The editMessage* 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.