Skip to main content

A Python library for converting Atlassian Document Format (ADF) to Markdown

Project description

pyadf

A high-performance Python library for converting Atlassian Document Format (ADF) to Markdown.

Features

  • Rust-powered — parsing and rendering run in native code via PyO3
  • Robust error handling with detailed, context-aware error messages
  • Type-safe with comprehensive type hints and Python 3.11+ support
  • Comprehensive node support:
    • Text formatting (bold, italic, links)
    • Headings (h1-h6)
    • Lists (bullet, ordered, task lists)
    • Tables with headers and column spans
    • Code blocks with syntax highlighting
    • Blockquotes and panels
    • Status badges, inline cards, block cards, emoji, mentions
  • Streaming JSONL API for ETL pipelines processing millions of documents

Installation

pip install pyadf

Prebuilt wheels are available for Linux and macOS (x86_64 and aarch64) and Windows (x86_64).

pyadf only supports Python version from 3.11.

Usage

Basic Usage

from pyadf import Document

adf_data = {
    "version": 1,
    "type": "doc",
    "content": [
        {
            "type": "paragraph",
            "content": [
                {"type": "text", "text": "Hello, "},
                {"type": "text", "text": "world!", "marks": [{"type": "strong"}]}
            ]
        }
    ]
}

doc = Document(adf_data)
print(doc.to_markdown())
# Output: Hello, **world!**

Converting from JSON String

from pyadf import Document

adf_json = '{"type": "doc", "content": [...]}'
doc = Document(adf_json)
markdown = doc.to_markdown()

Parsing Markdown to ADF

from pyadf import Document, markdown_to_adf

doc = Document.from_markdown("# Hello\n\nThis is **bold**.")
adf = doc.to_adf()

adf2 = markdown_to_adf("1. First\n2. Second")

The Markdown importer is currently strict and targets the canonical subset that pyadf already renders well.

Detailed ADF element and Markdown import policy lives in docs/adf-element-policy.md.

Converting Individual Nodes

from pyadf import Document

node = {
    "type": "heading",
    "attrs": {"level": 2},
    "content": [{"type": "text", "text": "My Heading"}]
}

doc = Document(node)
print(doc.to_markdown())
# Output: ## My Heading

Batch JSONL Processing

For ETL pipelines processing large volumes of ADF documents:

from pyadf import convert_jsonl, MarkdownConfig

# From a JSONL file (one ADF document per line)
for result in convert_jsonl("export.jsonl"):
    print(result)

# From bytes with custom config
config = MarkdownConfig(bullet_marker="*", show_links=True)
for result in convert_jsonl(jsonl_bytes, config=config, batch_size=10_000):
    print(result)

# Error handling modes
from pyadf import ConversionError

for result in convert_jsonl(data, on_error="include"):
    if isinstance(result, ConversionError):
        print(f"Line {result.line_number}: {result.error}")
    else:
        print(result)

convert_jsonl accepts:

  • source: file path (str), raw bytes, or a binary file-like object
  • config: optional MarkdownConfig
  • on_error: "include" (default, yields ConversionError), "skip", or "raise"
  • batch_size: lines per Rust batch (default 10,000)

Error Handling

from pyadf import Document, InvalidJSONError, UnsupportedNodeTypeError

try:
    doc = Document('invalid json')
except InvalidJSONError as e:
    print(f"Invalid JSON: {e}")

try:
    doc = Document({"type": "unsupported_type"})
except UnsupportedNodeTypeError as e:
    print(f"Unsupported node: {e}")

# Known unsupported nodes like "extension" can be skipped, warned on, error, or preserved as HTML at render time
doc = Document({"type": "extension"})
assert doc.to_markdown() == ""

doc = Document(
    {
        "type": "extension",
        "attrs": {"extensionKey": "toc", "extensionType": "com.atlassian.confluence.macro.core"},
    }
)
assert doc.to_markdown(on_known_unsupported="html") == (
    '<div adf="extension" '
    'params=\'{"extensionKey":"toc","extensionType":"com.atlassian.confluence.macro.core"}\'></div>'
)

Known unsupported node handling:

  • Document(...).to_markdown() defaults to on_known_unsupported="warn" and emits UserWarning while skipping known unsupported nodes such as extension
  • Document(...).to_markdown(on_known_unsupported="skip") silently skips known unsupported nodes
  • Document(...).to_markdown(on_known_unsupported="error") raises UnsupportedNodeTypeError
  • Document(...).to_markdown(on_known_unsupported="html") preserves known unsupported nodes as invisible HTML fallback elements like <div adf="extension" params='...'></div> (or <span ...></span> in inline/cell contexts)

The same on_known_unsupported option is available on convert_jsonl(...).

Customizing Markdown Output

from pyadf import Document, MarkdownConfig

doc = Document(adf_data)

# Default bullet marker is -
doc.to_markdown()  # "- Item 1\n- Item 2"

# Use * for bullet lists
config = MarkdownConfig(bullet_marker="*")
doc.to_markdown(config)  # "* Item 1\n* Item 2"

# Links are shown by default
doc.to_markdown()  # [Link text](http://example.com)

# Hide underlying href while keeping link text marked
config = MarkdownConfig(show_links=False)
doc.to_markdown(config)  # [Link text]
Option Values Default Description
bullet_marker +, -, * - Character used for bullet list items
show_links True, False True Show underlying links in markdown

Supported Markdown Import Subset

Document.from_markdown(...) and markdown_to_adf(...) currently support a small, strict subset of Markdown:

  • Paragraphs
  • ATX headings (# through ######)
  • Bold / italic / bold+italic
  • Inline links
  • Bullet and ordered lists
  • Blockquotes
  • Fenced code blocks
  • GFM tables
  • pyadf HTML fallback elements such as <div adf="extension" ...></div>

The importer intentionally rejects many other Markdown forms for now (for example generic HTML), so roundtrip behavior stays deterministic while the feature set is being expanded.

For the living ADF element and Markdown import policy, see docs/adf-element-policy.md.

Known Unsupported Nodes

These node types are recognized but not rendered. By default they are warned:

  • mediaSingle
  • mediaGroup
  • mediaInline
  • expand
  • rule
  • media
  • embedCard
  • extension

Supported ADF Node Types

ADF Node Type Markdown Output Notes
doc Document root Top-level container
paragraph Plain text with newlines
text Text with optional formatting Supports bold, italic, links
heading # Heading (levels 1-6)
bulletList - Item
orderedList 1. Item
taskList - [ ] Task Checkbox tasks
codeBlock ```language\ncode\n``` Optional language syntax
blockquote > Quote
panel > Panel content Info/warning/error boxes
table Markdown table Supports headers and colspan
status **[STATUS]** Status badges
inlineCard [link] or code block Link previews
emoji Unicode emoji
hardBreak Line break
mention @DisplayName Jira user mentions
blockCard [link] or code block Link previews

Exception Types

  • PyADFError — Base exception for all pyadf errors
  • InvalidJSONError — Raised when JSON parsing fails
  • InvalidInputError — Raised when input type is incorrect
  • InvalidADFError — Raised when ADF structure is invalid
  • MissingFieldError — Raised when required fields are missing
  • InvalidFieldError — Raised when field values are invalid
  • UnsupportedNodeTypeError — Raised when encountering unsupported node types
  • NodeCreationError — Raised when node creation fails

All exceptions include detailed context about the error location in the ADF tree.

Development

Prerequisites

  • Python 3.11+
  • Rust toolchain (stable)
  • maturin (uv tool install maturin)

Setup

git clone https://github.com/YoungseokCh/pyadf.git
cd pyadf
uv sync
uv run maturin develop

Testing

cargo test              # Rust unit tests
uv run pytest tests/ -v # Python tests

Linting

# Rust
cargo fmt --check
cargo clippy -- -D warnings

# Python
ruff check src/ tests/ benchmarks/
ruff format --check src/ tests/ benchmarks/

License

MIT License — see LICENSE file for details.

Changelog

0.5.1

  • Support 'version' property for top-level ADF Document node
  • Add Python 3.14 support

0.5.0

  • Move on_known_unsupported=error|skip|warn|html from Document(...) construction to Document(...).to_markdown(...)
  • Add on_known_unsupported="html" to render known unsupported nodes as invisible HTML fallback elements
  • Add Document.from_markdown(...) for strict Markdown -> ADF parsing
  • Add Document.to_adf() for exporting canonical ADF dictionaries
  • Expand Markdown import support for inline code, strikethrough, task lists with TODO / DONE state, nested lists, multi-paragraph list items, and canonical GFM tables with inline marks
  • Preserve taskList.attrs.localId and taskItem.attrs.localId/state when exporting ADF; Markdown import sets task state but does not generate localId
  • Canonicalize accepted Markdown variants such as underscore emphasis, URL autolinks, and code-block info strings while rejecting reference-style links
  • Tighten pyadf HTML fallback parsing by rejecting unclosed fallback wrappers and malformed params JSON

0.4.3

  • Show link targets by default in markdown output
  • Use - as the default bullet marker
  • Treat extension as a known unsupported node instead of failing by default
  • Add on_known_unsupported=error|skip|warn for known unsupported nodes; unknown node types still error

0.4.2

  • Add support for blockCard node type

0.4.1

  • Fix linux x86_64 wheel builds

0.4.0

  • Rust core via PyO3 — 5x faster single-doc, 24x faster batch processing
  • New convert_jsonl() streaming API for batch JSONL processing
  • New ConversionError dataclass for structured batch error handling
  • Build system switched from setuptools to maturin
  • abi3 stable ABI wheels for Linux, macOS (x86_64 + aarch64) and Windows (x86_64)

Breaking changes:

  • Removed set_debug_mode() and _logger module (will be replaced with Rust-native tracing in a future release)
  • nodes and _types modules removed (internal implementation replaced by Rust)

0.3.2

  • Added support for showing href links in markdown output

0.3.1

  • Added mention node support

0.3.0

  • Added emoji node support
  • Added configurable bullet markers via MarkdownConfig

0.1.0

  • Class-based API with Document class
  • Support for common ADF node types
  • Type-safe architecture with comprehensive type hints (Python 3.11+)
  • Flexible input handling (JSON strings, dictionaries, individual nodes)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyadf-0.5.1.tar.gz (60.2 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pyadf-0.5.1-cp311-abi3-win_amd64.whl (409.4 kB view details)

Uploaded CPython 3.11+Windows x86-64

pyadf-0.5.1-cp311-abi3-manylinux_2_34_x86_64.whl (503.6 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.34+ x86-64

pyadf-0.5.1-cp311-abi3-manylinux_2_28_x86_64.whl (504.0 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.28+ x86-64

pyadf-0.5.1-cp311-abi3-manylinux_2_28_aarch64.whl (486.6 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.28+ ARM64

pyadf-0.5.1-cp311-abi3-macosx_11_0_arm64.whl (447.8 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

pyadf-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl (470.7 kB view details)

Uploaded CPython 3.11+macOS 10.12+ x86-64

File details

Details for the file pyadf-0.5.1.tar.gz.

File metadata

  • Download URL: pyadf-0.5.1.tar.gz
  • Upload date:
  • Size: 60.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyadf-0.5.1.tar.gz
Algorithm Hash digest
SHA256 da112e596a46ad11995a4a8e1dcac5f280810e3b71b7f883dfbb78bf8f8cd9f1
MD5 4b7f9391a2272ed57219f6f7c8e3143c
BLAKE2b-256 3eb1f64a2ca3b785f5d1ad8f3729977579277ee60c24c104caf3e9536de2b96b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyadf-0.5.1.tar.gz:

Publisher: publish.yml on YoungseokCh/pyadf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyadf-0.5.1-cp311-abi3-win_amd64.whl.

File metadata

  • Download URL: pyadf-0.5.1-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 409.4 kB
  • Tags: CPython 3.11+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyadf-0.5.1-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 7e603e8ab2976850b6c3c92b93989377b6dbabc1326f7393ecedd9dded8e446f
MD5 0d234578e3eb393f8df7640be75f78e2
BLAKE2b-256 02b3d7c6d0a6bcf620125e1eb51c315454f2fb434f8bbacf8d8b1dfd238874a9

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyadf-0.5.1-cp311-abi3-win_amd64.whl:

Publisher: publish.yml on YoungseokCh/pyadf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyadf-0.5.1-cp311-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for pyadf-0.5.1-cp311-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 0fd3bd266169966361eebe48bfd5837f1e47566e52ff157c0b29f5b229a1e7c2
MD5 b7598bf92fadd031d3b0ff1089eb7b0c
BLAKE2b-256 8e2f1296334c5fb491e1239d7f9097e13180609316dc41b1c0215a7c7375e139

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyadf-0.5.1-cp311-abi3-manylinux_2_34_x86_64.whl:

Publisher: publish.yml on YoungseokCh/pyadf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyadf-0.5.1-cp311-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for pyadf-0.5.1-cp311-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 fdfbd3820fefc30984ae1700c5686b141fab6695a405c7510eed348e00681dc3
MD5 7ed9792cf466b61b7b4de896ca566f11
BLAKE2b-256 81467273753b606411158a8ffeee4406398222ae1d234d80877be4cf286c9051

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyadf-0.5.1-cp311-abi3-manylinux_2_28_x86_64.whl:

Publisher: publish.yml on YoungseokCh/pyadf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyadf-0.5.1-cp311-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for pyadf-0.5.1-cp311-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 f8c004b0a6837c9ff6fb11163bc6e90ddf6ac3dd9a494e764b87e78bbb09a1e4
MD5 b4ef166a9827465ff67caa097b4a9027
BLAKE2b-256 af300387e4e912e45f76c0dcad23d936b2a8c82f047fb18cea6f99bff86a5c10

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyadf-0.5.1-cp311-abi3-manylinux_2_28_aarch64.whl:

Publisher: publish.yml on YoungseokCh/pyadf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyadf-0.5.1-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

  • Download URL: pyadf-0.5.1-cp311-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 447.8 kB
  • Tags: CPython 3.11+, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyadf-0.5.1-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 317321ee56e72b0729da04cb90347a9a67886020e057cd20aedc81a35c244333
MD5 e88bc60c1892d28ad8129b2a931cadbe
BLAKE2b-256 3f2732bafcd0f93bdcbdaa61b3fc46df8d20be1a3a6b0ed74b9e6f75a8b913ce

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyadf-0.5.1-cp311-abi3-macosx_11_0_arm64.whl:

Publisher: publish.yml on YoungseokCh/pyadf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyadf-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pyadf-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 c00fca0bfec33325b55cd69eca67aa08aaf20a2d1bccdbecaa66e59218327a02
MD5 daed5faec7a1a909d1491d1f69b87911
BLAKE2b-256 df5823b3259812701d8cdf21e445b5a60a7c61a6e4dc3d04e83599288d0dd279

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyadf-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl:

Publisher: publish.yml on YoungseokCh/pyadf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page